From d3261e64152397db2dca4d691a990c6bc2a6f4dd Mon Sep 17 00:00:00 2001 From: Mehmet Samet Duman Date: Thu, 2 Apr 2026 18:51:45 +0300 Subject: NOISSUE add archived projects Signed-off-by: Mehmet Samet Duman --- archived/projt-launcher/.clang-format | 230 + archived/projt-launcher/.clang-format-ignore | 1 + archived/projt-launcher/.clang-tidy | 48 + .../projt-launcher/.clusterfuzzlite/Dockerfile | 15 + archived/projt-launcher/.clusterfuzzlite/build.sh | 45 + archived/projt-launcher/.editorconfig | 20 + archived/projt-launcher/.envrc | 2 + archived/projt-launcher/.gitattributes | 89 + archived/projt-launcher/.gitignore | 553 ++ archived/projt-launcher/.markdownlint.yaml | 19 + archived/projt-launcher/.markdownlintignore | 29 + archived/projt-launcher/.python-version | 1 + archived/projt-launcher/CHANGELOG.md | 65 + archived/projt-launcher/CMakeLists.txt | 854 ++ archived/projt-launcher/CMakePresets.json | 326 + archived/projt-launcher/CODE_OF_CONDUCT | 132 + archived/projt-launcher/COPYING.md | 345 + archived/projt-launcher/Containerfile | 74 + archived/projt-launcher/MAINTAINERS | 19 + archived/projt-launcher/README | 125 + .../projt-launcher/bootstrap/macos/Bootstrap.m | 423 + .../projt-launcher/bootstrap/macos/CMakeLists.txt | 38 + .../projt-launcher/bootstrap/macos/Info.plist.in | 32 + .../projt-launcher/buildconfig/BuildConfig.cpp.in | 183 + archived/projt-launcher/buildconfig/BuildConfig.h | 260 + archived/projt-launcher/buildconfig/CMakeLists.txt | 11 + archived/projt-launcher/buildconfig/Makefile | 60 + archived/projt-launcher/ci/code-quality.nix | 51 + archived/projt-launcher/ci/code-quality.sh | 156 + .../ci/codeowners-validator/default.nix | 52 + .../ci/codeowners-validator/owners-file-name.patch | 15 + .../ci/codeowners-validator/permissions.patch | 36 + archived/projt-launcher/ci/default.nix | 68 + archived/projt-launcher/ci/eval/attrpaths.nix | 147 + archived/projt-launcher/ci/eval/chunk.nix | 69 + .../projt-launcher/ci/eval/compare/cmp-stats.py | 218 + .../projt-launcher/ci/eval/compare/default.nix | 134 + .../ci/eval/compare/generate-step-summary.jq | 70 + .../projt-launcher/ci/eval/compare/maintainers.nix | 142 + archived/projt-launcher/ci/eval/compare/utils.nix | 198 + archived/projt-launcher/ci/eval/default.nix | 316 + archived/projt-launcher/ci/eval/diff.nix | 123 + archived/projt-launcher/ci/eval/outpaths.nix | 133 + .../projt-launcher/ci/github-script/.editorconfig | 3 + .../projt-launcher/ci/github-script/.gitignore | 2 + archived/projt-launcher/ci/github-script/.npmrc | 2 + .../projt-launcher/ci/github-script/backport.js | 688 ++ .../ci/github-script/commit-types.json | 70 + .../projt-launcher/ci/github-script/commits.js | 336 + .../projt-launcher/ci/github-script/get-teams.js | 134 + archived/projt-launcher/ci/github-script/merge.js | 308 + .../ci/github-script/package-lock.json | 1721 ++++ .../projt-launcher/ci/github-script/package.json | 19 + .../projt-launcher/ci/github-script/prepare.js | 314 + .../projt-launcher/ci/github-script/reviewers.js | 329 + .../projt-launcher/ci/github-script/reviews.js | 93 + archived/projt-launcher/ci/github-script/run | 132 + archived/projt-launcher/ci/github-script/shell.nix | 40 + .../ci/github-script/test/commits.test.js | 42 + .../ci/github-script/withRateLimit.js | 86 + archived/projt-launcher/ci/nixpkgs-vet.nix | 2 + archived/projt-launcher/ci/nixpkgs-vet.sh | 6 + archived/projt-launcher/ci/parse.nix | 38 + archived/projt-launcher/ci/pinned.json | 44 + archived/projt-launcher/ci/supportedBranches.js | 99 + archived/projt-launcher/ci/supportedSystems.json | 64 + archived/projt-launcher/ci/supportedVersions.nix | 68 + archived/projt-launcher/ci/update-pinned.sh | 64 + .../projt-launcher/cmake/CompilerWarnings.cmake | 164 + archived/projt-launcher/cmake/ECMQueryQt.cmake | 100 + .../cmake/GetGitRevisionDescription.cmake | 284 + .../cmake/GetGitRevisionDescription.cmake.in | 43 + archived/projt-launcher/cmake/GitFunctions.cmake | 37 + .../projt-launcher/cmake/LauncherPackaging.cmake | 72 + .../projt-launcher/cmake/MacOSXBundleInfo.plist.in | 94 + .../projt-launcher/cmake/QtVersionOption.cmake | 38 + .../cmake/QtVersionlessBackport.cmake | 97 + .../projt-launcher/cmake/findOpenSSLbyNuget.cmake | 34 + archived/projt-launcher/cmake/useBZip2.cmake | 45 + archived/projt-launcher/cmake/useCMark.cmake | 50 + archived/projt-launcher/cmake/useLibpng.cmake | 48 + archived/projt-launcher/cmake/useLibqrencode.cmake | 91 + archived/projt-launcher/cmake/useMinimalLibs.cmake | 60 + archived/projt-launcher/cmake/useMinizip.cmake | 26 + archived/projt-launcher/cmake/usePTlibzippy.cmake | 96 + archived/projt-launcher/cmake/useQuazip.cmake | 31 + .../projt-launcher/cmake/useTomlplusplus.cmake | 21 + .../cmake/vcpkg-ports/vcpkg-tool-meson/README.md | 3 + .../vcpkg-ports/vcpkg-tool-meson/adjust-args.patch | 13 + .../vcpkg-tool-meson/adjust-python-dep.patch | 45 + .../fix-libcpp-enable-assertions.patch | 52 + .../vcpkg-ports/vcpkg-tool-meson/install.cmake | 5 + .../vcpkg-ports/vcpkg-tool-meson/meson-intl.patch | 13 + .../vcpkg-ports/vcpkg-tool-meson/meson.template.in | 43 + .../vcpkg-ports/vcpkg-tool-meson/portfile.cmake | 45 + .../remove-freebsd-pcfile-specialization.patch | 23 + .../vcpkg-tool-meson/universal-osx.patch | 16 + .../vcpkg-tool-meson/vcpkg-port-config.cmake | 62 + .../cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json | 11 + .../vcpkg-tool-meson/vcpkg_configure_meson.cmake | 480 + .../vcpkg-tool-meson/vcpkg_install_meson.cmake | 71 + .../cmake/vcpkg-triplets/universal-osx.cmake | 8 + archived/projt-launcher/default.nix | 4 + .../projt-launcher/docs/APPLE_SILICON_RATIONALE.md | 46 + archived/projt-launcher/docs/BUILD_SYSTEM.md | 996 ++ archived/projt-launcher/docs/FUZZING.md | 39 + archived/projt-launcher/docs/README.md | 19 + .../projt-launcher/docs/architecture/OVERVIEW.md | 166 + .../docs/contributing/ARCHITECTURE.md | 177 + .../projt-launcher/docs/contributing/CODE_STYLE.md | 230 + .../docs/contributing/GETTING_STARTED.md | 222 + .../docs/contributing/LAUNCHER_TEST_MATRIX.md | 51 + .../docs/contributing/PROJECT_STRUCTURE.md | 123 + .../projt-launcher/docs/contributing/README.md | 19 + .../projt-launcher/docs/contributing/TESTING.md | 149 + .../projt-launcher/docs/contributing/WORKFLOW.md | 142 + archived/projt-launcher/docs/handbook/README.md | 65 + archived/projt-launcher/docs/handbook/bot.md | 198 + .../docs/handbook/bzip2-compiling.md | 163 + .../docs/handbook/bzip2-testfiles.md | 101 + .../projt-launcher/docs/handbook/bzip2-tests.md | 80 + archived/projt-launcher/docs/handbook/bzip2.md | 124 + .../projt-launcher/docs/handbook/ci_support.md | 103 + archived/projt-launcher/docs/handbook/cmark.md | 179 + .../docs/handbook/extra-cmake-modules.md | 67 + .../projt-launcher/docs/handbook/help-pages.md | 37 + .../docs/handbook/help-pages/apis.md | 43 + .../docs/handbook/help-pages/atl-platform.md | 5 + .../docs/handbook/help-pages/custom-commands.md | 16 + .../handbook/help-pages/environment-variables.md | 5 + .../docs/handbook/help-pages/flame-platform.md | 7 + .../docs/handbook/help-pages/ftb-platform.md | 7 + .../docs/handbook/help-pages/index.md | 39 + .../docs/handbook/help-pages/instance-copy.md | 75 + .../docs/handbook/help-pages/instance-version.md | 35 + .../docs/handbook/help-pages/java-settings.md | 40 + .../docs/handbook/help-pages/java-wizard.md | 11 + .../docs/handbook/help-pages/language-settings.md | 5 + .../docs/handbook/help-pages/launcher-settings.md | 61 + .../docs/handbook/help-pages/loader-mods.md | 9 + .../docs/handbook/help-pages/minecraft-settings.md | 35 + .../docs/handbook/help-pages/mod-platform.md | 15 + .../docs/handbook/help-pages/modrinth-platform.md | 5 + .../docs/handbook/help-pages/notes.md | 5 + .../docs/handbook/help-pages/proxy-settings.md | 21 + .../handbook/help-pages/screenshots-management.md | 5 + .../docs/handbook/help-pages/technic-platform.md | 5 + .../docs/handbook/help-pages/tools.md | 29 + .../docs/handbook/help-pages/vanilla-platform.md | 5 + .../docs/handbook/help-pages/worlds.md | 5 + .../docs/handbook/help-pages/zip-import.md | 5 + archived/projt-launcher/docs/handbook/images.md | 66 + archived/projt-launcher/docs/handbook/javacheck.md | 86 + .../projt-launcher/docs/handbook/launcherjava.md | 142 + .../projt-launcher/docs/handbook/libnbtplusplus.md | 188 + .../projt-launcher/docs/handbook/libqrencode.md | 221 + .../docs/handbook/linux-packaging.md | 121 + archived/projt-launcher/docs/handbook/nix.md | 212 + .../projt-launcher/docs/handbook/program_info.md | 65 + archived/projt-launcher/docs/handbook/ptcieval.md | 122 + archived/projt-launcher/docs/handbook/ptcigh.md | 95 + archived/projt-launcher/docs/handbook/quazip.md | 149 + .../projt-launcher/docs/handbook/third-party.md | 166 + .../projt-launcher/docs/handbook/tomlplusplus.md | 229 + .../docs/handbook/website-tomlplusplus.md | 124 + .../docs/handbook/wiki/development/index.md | 21 + .../wiki/development/instructions/index.md | 23 + .../wiki/development/instructions/linux.md | 260 + .../wiki/development/instructions/macos.md | 162 + .../wiki/development/instructions/windows.md | 201 + .../docs/handbook/wiki/development/translating.md | 16 + .../docs/handbook/wiki/getting-started/catpacks.md | 121 + .../wiki/getting-started/command-line-interface.md | 33 + .../wiki/getting-started/controller-support.md | 52 + .../wiki/getting-started/create-instance.md | 13 + .../handbook/wiki/getting-started/data-location.md | 32 + .../wiki/getting-started/download-modpacks.md | 42 + .../handbook/wiki/getting-started/download-mods.md | 19 + .../docs/handbook/wiki/getting-started/index.md | 13 + .../getting-started/install-of-alternatives.md | 119 + .../wiki/getting-started/installing-java.md | 179 + .../wiki/getting-started/installing-optifine.md | 66 + .../getting-started/installing-projtlauncher.md | 5 + .../getting-started/migrating-prismlauncher.md | 31 + .../docs/handbook/wiki/getting-started/settings.md | 49 + .../docs/handbook/wiki/help-pages/apis.md | 45 + .../docs/handbook/wiki/help-pages/atl-platform.md | 7 + .../handbook/wiki/help-pages/custom-commands.md | 18 + .../wiki/help-pages/environment-variables.md | 7 + .../handbook/wiki/help-pages/flame-platform.md | 9 + .../docs/handbook/wiki/help-pages/ftb-platform.md | 9 + .../docs/handbook/wiki/help-pages/index.md | 41 + .../docs/handbook/wiki/help-pages/instance-copy.md | 77 + .../handbook/wiki/help-pages/instance-version.md | 37 + .../docs/handbook/wiki/help-pages/java-settings.md | 42 + .../docs/handbook/wiki/help-pages/java-wizard.md | 13 + .../handbook/wiki/help-pages/language-settings.md | 7 + .../handbook/wiki/help-pages/launcher-settings.md | 63 + .../docs/handbook/wiki/help-pages/loader-mods.md | 11 + .../handbook/wiki/help-pages/minecraft-settings.md | 37 + .../docs/handbook/wiki/help-pages/mod-platform.md | 17 + .../handbook/wiki/help-pages/modrinth-platform.md | 7 + .../docs/handbook/wiki/help-pages/notes.md | 7 + .../handbook/wiki/help-pages/proxy-settings.md | 23 + .../wiki/help-pages/screenshots-management.md | 7 + .../handbook/wiki/help-pages/technic-platform.md | 7 + .../docs/handbook/wiki/help-pages/tools.md | 31 + .../handbook/wiki/help-pages/vanilla-platform.md | 7 + .../docs/handbook/wiki/help-pages/worlds.md | 7 + .../docs/handbook/wiki/help-pages/zip-import.md | 7 + .../docs/handbook/wiki/overview/code-of-conduct.md | 91 + .../docs/handbook/wiki/overview/copying.md | 821 ++ .../docs/handbook/wiki/overview/faq.md | 9 + .../docs/handbook/wiki/overview/feedback-bugs.md | 9 + .../docs/handbook/wiki/overview/frequent-issues.md | 78 + .../docs/handbook/wiki/overview/index.md | 11 + .../projt-launcher/docs/handbook/wiki/wiki.json | 3 + archived/projt-launcher/docs/handbook/workflows.md | 226 + archived/projt-launcher/docs/handbook/zlib.md | 134 + archived/projt-launcher/docs/packaging/README.md | 8 + .../os-specific/linux/flathub/flathubpackage.yml | 110 + .../os-specific/linux/flathub/projtlauncher | 11 + .../linux/nix/projtlauncher-unwrapped.nix | 109 + .../os-specific/linux/nix/projtlauncher.nix | 132 + .../os-specific/macos/homebrew/projtlauncher.rb | 31 + archived/projt-launcher/flake.lock | 27 + archived/projt-launcher/flake.nix | 217 + archived/projt-launcher/flatpak/.editorconfig | 21 + archived/projt-launcher/flatpak/.gitignore | 5 + archived/projt-launcher/flatpak/.gitmodules | 3 + archived/projt-launcher/flatpak/README.md | 36 + archived/projt-launcher/flatpak/modules/flite.yml | 15 + archived/projt-launcher/flatpak/modules/glfw.yml | 27 + .../projt-launcher/flatpak/modules/glxinfo.yml | 26 + archived/projt-launcher/flatpak/modules/inih.yml | 6 + archived/projt-launcher/flatpak/modules/xrandr.yml | 14 + .../flatpak/org.projecttick.ProjTLauncher.yml | 82 + .../flatpak/patches/0001-Ez-peazy.flite.patch | 25 + ...land-Partially-implement-glfwSetCursorPos.patch | 88 + .../0002-Wayland-Implement-glfwSetWindowIcon.patch | 386 + ...n-though-no-window-icon-support-on-waylan.patch | 30 + archived/projt-launcher/flatpak/pubkey.asc | 13 + archived/projt-launcher/flatpak/scripts/prime-run | 4 + .../projt-launcher/flatpak/scripts/projtlauncher | 11 + archived/projt-launcher/flatpak/static/index.html | 23 + .../static/projtlauncher-nightly.flatpakref | 8 + .../flatpak/static/projtlauncher.flatpakrepo | 8 + archived/projt-launcher/fuzz/CMakeLists.txt | 39 + archived/projt-launcher/fuzz/fuzz_gzip.cpp | 17 + archived/projt-launcher/fuzz/fuzz_nbt_reader.cpp | 36 + archived/projt-launcher/fuzz/fuzz_qjson_parse.cpp | 17 + .../fuzz/fuzz_separator_prefix_tree.cpp | 123 + archived/projt-launcher/garnix.yaml | 5 + archived/projt-launcher/launcher/Application.cpp | 2450 +++++ archived/projt-launcher/launcher/Application.h | 402 + .../projt-launcher/launcher/ApplicationMessage.cpp | 89 + .../projt-launcher/launcher/ApplicationMessage.h | 36 + archived/projt-launcher/launcher/BaseInstaller.cpp | 80 + archived/projt-launcher/launcher/BaseInstaller.h | 66 + archived/projt-launcher/launcher/BaseInstance.cpp | 526 ++ archived/projt-launcher/launcher/BaseInstance.h | 427 + archived/projt-launcher/launcher/BaseVersion.h | 79 + .../projt-launcher/launcher/BaseVersionList.cpp | 147 + archived/projt-launcher/launcher/BaseVersionList.h | 142 + archived/projt-launcher/launcher/CMakeLists.txt | 2412 +++++ archived/projt-launcher/launcher/CefRuntime.cpp | 177 + archived/projt-launcher/launcher/CefRuntime.h | 47 + archived/projt-launcher/launcher/Commandline.cpp | 116 + archived/projt-launcher/launcher/Commandline.h | 56 + .../projt-launcher/launcher/DataMigrationTask.cpp | 119 + .../projt-launcher/launcher/DataMigrationTask.h | 64 + archived/projt-launcher/launcher/DefaultVariable.h | 56 + .../projt-launcher/launcher/DesktopServices.cpp | 110 + archived/projt-launcher/launcher/DesktopServices.h | 68 + archived/projt-launcher/launcher/Exception.h | 86 + .../projt-launcher/launcher/ExponentialSeries.h | 62 + archived/projt-launcher/launcher/FileSystem.cpp | 2002 ++++ archived/projt-launcher/launcher/FileSystem.h | 650 ++ archived/projt-launcher/launcher/Filter.h | 77 + archived/projt-launcher/launcher/GZip.cpp | 253 + archived/projt-launcher/launcher/GZip.h | 32 + archived/projt-launcher/launcher/HardwareInfo.cpp | 331 + archived/projt-launcher/launcher/HardwareInfo.h | 30 + .../projt-launcher/launcher/InstanceCopyPrefs.cpp | 212 + .../projt-launcher/launcher/InstanceCopyPrefs.h | 75 + .../projt-launcher/launcher/InstanceCopyTask.cpp | 287 + .../projt-launcher/launcher/InstanceCopyTask.h | 61 + .../launcher/InstanceCreationTask.cpp | 155 + .../projt-launcher/launcher/InstanceCreationTask.h | 86 + .../projt-launcher/launcher/InstanceDirUpdate.cpp | 168 + .../projt-launcher/launcher/InstanceDirUpdate.h | 68 + .../projt-launcher/launcher/InstanceImportTask.cpp | 648 ++ .../projt-launcher/launcher/InstanceImportTask.h | 110 + archived/projt-launcher/launcher/InstanceList.cpp | 1293 +++ archived/projt-launcher/launcher/InstanceList.h | 280 + .../projt-launcher/launcher/InstancePageProvider.h | 85 + archived/projt-launcher/launcher/InstanceTask.cpp | 109 + archived/projt-launcher/launcher/InstanceTask.h | 138 + archived/projt-launcher/launcher/JavaCommon.cpp | 151 + archived/projt-launcher/launcher/JavaCommon.h | 74 + archived/projt-launcher/launcher/Json.cpp | 388 + archived/projt-launcher/launcher/Json.h | 332 + archived/projt-launcher/launcher/Kconfig | 287 + archived/projt-launcher/launcher/KonamiCode.cpp | 63 + archived/projt-launcher/launcher/KonamiCode.h | 37 + .../projt-launcher/launcher/LaunchController.cpp | 601 ++ .../projt-launcher/launcher/LaunchController.h | 166 + archived/projt-launcher/launcher/LaunchMode.h | 28 + archived/projt-launcher/launcher/Launcher.in | 106 + archived/projt-launcher/launcher/LoggedProcess.cpp | 218 + archived/projt-launcher/launcher/LoggedProcess.h | 124 + archived/projt-launcher/launcher/MMCTime.cpp | 133 + archived/projt-launcher/launcher/MMCTime.h | 56 + archived/projt-launcher/launcher/MMCZip.cpp | 872 ++ archived/projt-launcher/launcher/MMCZip.h | 282 + archived/projt-launcher/launcher/MTPixmapCache.h | 208 + archived/projt-launcher/launcher/MangoHud.cpp | 213 + archived/projt-launcher/launcher/MangoHud.h | 51 + archived/projt-launcher/launcher/Markdown.cpp | 53 + archived/projt-launcher/launcher/Markdown.h | 45 + archived/projt-launcher/launcher/MessageLevel.cpp | 89 + archived/projt-launcher/launcher/MessageLevel.h | 54 + archived/projt-launcher/launcher/NullInstance.h | 148 + archived/projt-launcher/launcher/PSaveFile.h | 103 + archived/projt-launcher/launcher/PngReader.cpp | 223 + archived/projt-launcher/launcher/PngReader.h | 83 + archived/projt-launcher/launcher/ProblemProvider.h | 71 + archived/projt-launcher/launcher/QObjectPtr.h | 80 + archived/projt-launcher/launcher/QVariantUtils.h | 96 + archived/projt-launcher/launcher/RWStorage.h | 89 + .../launcher/RecursiveFileSystemWatcher.cpp | 129 + .../launcher/RecursiveFileSystemWatcher.h | 81 + .../launcher/ResourceDownloadTask.cpp | 157 + .../projt-launcher/launcher/ResourceDownloadTask.h | 109 + archived/projt-launcher/launcher/RuntimeContext.h | 97 + .../projt-launcher/launcher/SeparatorPrefixTree.h | 319 + archived/projt-launcher/launcher/StringUtils.cpp | 285 + archived/projt-launcher/launcher/StringUtils.h | 117 + archived/projt-launcher/launcher/SysInfo.cpp | 121 + archived/projt-launcher/launcher/SysInfo.h | 36 + archived/projt-launcher/launcher/Untar.cpp | 317 + archived/projt-launcher/launcher/Untar.h | 72 + archived/projt-launcher/launcher/Usable.h | 86 + archived/projt-launcher/launcher/Version.cpp | 156 + archived/projt-launcher/launcher/Version.h | 232 + .../projt-launcher/launcher/VersionProxyModel.cpp | 552 ++ .../projt-launcher/launcher/VersionProxyModel.h | 108 + archived/projt-launcher/launcher/WatchLock.h | 38 + .../projt-launcher/launcher/console/Console.hpp | 52 + .../launcher/console/ConsoleStub.cpp | 27 + .../launcher/console/WindowsConsole.cpp | 176 + .../launcher/console/WindowsConsole.hpp | 31 + .../projt-launcher/launcher/filelink/FileLink.cpp | 267 + .../projt-launcher/launcher/filelink/FileLink.hpp | 74 + .../launcher/filelink/filelink.exe.manifest | 28 + .../launcher/filelink/filelink_main.cpp | 40 + .../projt-launcher/launcher/icons/IconEntry.cpp | 134 + .../projt-launcher/launcher/icons/IconEntry.hpp | 72 + .../projt-launcher/launcher/icons/IconList.cpp | 552 ++ .../projt-launcher/launcher/icons/IconList.hpp | 105 + .../projt-launcher/launcher/icons/IconUtils.cpp | 61 + .../projt-launcher/launcher/icons/IconUtils.hpp | 35 + .../launcher/java/core/RuntimeInstall.cpp | 86 + .../launcher/java/core/RuntimeInstall.hpp | 70 + .../launcher/java/core/RuntimePackage.cpp | 142 + .../launcher/java/core/RuntimePackage.hpp | 82 + .../launcher/java/core/RuntimeVersion.cpp | 212 + .../launcher/java/core/RuntimeVersion.hpp | 87 + .../launcher/java/download/RuntimeArchiveTask.cpp | 174 + .../launcher/java/download/RuntimeArchiveTask.hpp | 55 + .../launcher/java/download/RuntimeLinkTask.cpp | 101 + .../launcher/java/download/RuntimeLinkTask.hpp | 40 + .../launcher/java/download/RuntimeManifestTask.cpp | 184 + .../launcher/java/download/RuntimeManifestTask.hpp | 55 + .../launcher/java/services/RuntimeCatalog.cpp | 205 + .../launcher/java/services/RuntimeCatalog.hpp | 97 + .../launcher/java/services/RuntimeEnvironment.cpp | 105 + .../launcher/java/services/RuntimeEnvironment.hpp | 30 + .../launcher/java/services/RuntimeProbeTask.cpp | 297 + .../launcher/java/services/RuntimeProbeTask.hpp | 93 + .../launcher/java/services/RuntimeScanner.cpp | 441 + .../launcher/java/services/RuntimeScanner.hpp | 52 + .../launcher/launch/LaunchLineRouter.cpp | 117 + .../launcher/launch/LaunchLineRouter.hpp | 52 + .../launcher/launch/LaunchLogModel.cpp | 213 + .../launcher/launch/LaunchLogModel.hpp | 87 + .../launcher/launch/LaunchPipeline.cpp | 270 + .../launcher/launch/LaunchPipeline.hpp | 120 + .../projt-launcher/launcher/launch/LaunchStage.cpp | 39 + .../projt-launcher/launcher/launch/LaunchStage.hpp | 60 + .../launcher/launch/LaunchVariableExpander.cpp | 108 + .../launcher/launch/LaunchVariableExpander.hpp | 34 + .../launcher/launch/TaskBridgeStage.cpp | 96 + .../launcher/launch/TaskBridgeStage.hpp | 49 + .../launcher/launch/steps/HostLookupReportStep.cpp | 99 + .../launcher/launch/steps/HostLookupReportStep.hpp | 55 + .../launcher/launch/steps/LaunchCommandStep.cpp | 128 + .../launcher/launch/steps/LaunchCommandStep.hpp | 64 + .../launcher/launch/steps/LogMessageStep.cpp | 54 + .../launcher/launch/steps/LogMessageStep.hpp | 46 + .../launcher/launch/steps/QuitAfterGameStep.cpp | 36 + .../launcher/launch/steps/QuitAfterGameStep.hpp | 41 + .../launcher/launch/steps/RuntimeProbeStep.cpp | 186 + .../launcher/launch/steps/RuntimeProbeStep.hpp | 74 + .../launch/steps/ServerJoinResolveStep.cpp | 133 + .../launch/steps/ServerJoinResolveStep.hpp | 62 + .../launcher/logs/LogEventParser.cpp | 372 + .../launcher/logs/LogEventParser.hpp | 90 + .../projt-launcher/launcher/logs/LogRedactor.cpp | 86 + .../projt-launcher/launcher/logs/LogRedactor.hpp | 30 + archived/projt-launcher/launcher/main.cpp | 139 + .../projt-launcher/launcher/meta/BaseEntity.cpp | 291 + .../projt-launcher/launcher/meta/BaseEntity.hpp | 118 + archived/projt-launcher/launcher/meta/Index.cpp | 195 + archived/projt-launcher/launcher/meta/Index.hpp | 96 + .../projt-launcher/launcher/meta/JsonFormat.cpp | 223 + .../projt-launcher/launcher/meta/JsonFormat.hpp | 86 + archived/projt-launcher/launcher/meta/Version.cpp | 151 + archived/projt-launcher/launcher/meta/Version.hpp | 164 + .../projt-launcher/launcher/meta/VersionList.cpp | 351 + .../projt-launcher/launcher/meta/VersionList.hpp | 137 + archived/projt-launcher/launcher/minecraft/Agent.h | 56 + .../launcher/minecraft/AssetsUtils.cpp | 643 ++ .../launcher/minecraft/AssetsUtils.h | 153 + .../launcher/minecraft/BackupManager.cpp | 594 ++ .../launcher/minecraft/BackupManager.h | 148 + .../launcher/minecraft/Component.cpp | 576 ++ .../projt-launcher/launcher/minecraft/Component.h | 187 + .../launcher/minecraft/ComponentUpdateTask.cpp | 1114 +++ .../launcher/minecraft/ComponentUpdateTask.h | 62 + .../launcher/minecraft/ComponentUpdateTask_p.h | 55 + .../launcher/minecraft/GradleSpecifier.h | 219 + .../launcher/minecraft/LaunchProfile.cpp | 461 + .../launcher/minecraft/LaunchProfile.h | 184 + .../projt-launcher/launcher/minecraft/Library.cpp | 511 + .../projt-launcher/launcher/minecraft/Library.h | 285 + .../projt-launcher/launcher/minecraft/Logging.cpp | 48 + .../projt-launcher/launcher/minecraft/Logging.h | 49 + .../launcher/minecraft/MinecraftInstance.cpp | 1430 +++ .../launcher/minecraft/MinecraftInstance.h | 212 + .../minecraft/MinecraftInstanceLaunchMenu.cpp | 84 + .../minecraft/MinecraftInstanceLaunchMenu.h | 36 + .../launcher/minecraft/MinecraftLoadAndCheck.cpp | 71 + .../launcher/minecraft/MinecraftLoadAndCheck.h | 61 + .../launcher/minecraft/MojangDownloadInfo.h | 100 + .../launcher/minecraft/MojangVersionFormat.cpp | 461 + .../launcher/minecraft/MojangVersionFormat.h | 46 + .../launcher/minecraft/OneSixVersionFormat.cpp | 513 + .../launcher/minecraft/OneSixVersionFormat.h | 53 + .../launcher/minecraft/PackProfile.cpp | 1311 +++ .../launcher/minecraft/PackProfile.h | 244 + .../launcher/minecraft/PackProfile_p.h | 49 + .../launcher/minecraft/ParseUtils.cpp | 54 + .../projt-launcher/launcher/minecraft/ParseUtils.h | 29 + .../launcher/minecraft/ProfileUtils.cpp | 222 + .../launcher/minecraft/ProfileUtils.h | 83 + .../projt-launcher/launcher/minecraft/Rule.cpp | 119 + archived/projt-launcher/launcher/minecraft/Rule.h | 95 + .../launcher/minecraft/ShortcutUtils.cpp | 324 + .../launcher/minecraft/ShortcutUtils.h | 94 + .../minecraft/VanillaInstanceCreationTask.cpp | 59 + .../minecraft/VanillaInstanceCreationTask.h | 44 + .../launcher/minecraft/VersionFile.cpp | 116 + .../launcher/minecraft/VersionFile.h | 153 + .../launcher/minecraft/VersionFilterData.cpp | 89 + .../launcher/minecraft/VersionFilterData.h | 51 + .../projt-launcher/launcher/minecraft/World.cpp | 652 ++ archived/projt-launcher/launcher/minecraft/World.h | 162 + .../launcher/minecraft/WorldList.cpp | 494 + .../projt-launcher/launcher/minecraft/WorldList.h | 162 + .../launcher/minecraft/auth/AccountData.cpp | 479 + .../launcher/minecraft/auth/AccountData.hpp | 190 + .../launcher/minecraft/auth/AccountList.cpp | 776 ++ .../launcher/minecraft/auth/AccountList.hpp | 202 + .../launcher/minecraft/auth/AuthFlow.cpp | 438 + .../launcher/minecraft/auth/AuthFlow.hpp | 107 + .../launcher/minecraft/auth/AuthSession.cpp | 63 + .../launcher/minecraft/auth/AuthSession.hpp | 69 + .../launcher/minecraft/auth/MinecraftAccount.cpp | 374 + .../launcher/minecraft/auth/MinecraftAccount.hpp | 243 + .../launcher/minecraft/auth/Parsers.cpp | 597 ++ .../launcher/minecraft/auth/Parsers.hpp | 40 + .../launcher/minecraft/auth/steps/Credentials.hpp | 233 + .../minecraft/auth/steps/DeviceCodeAuthStep.cpp | 323 + .../minecraft/auth/steps/DeviceCodeAuthStep.hpp | 83 + .../minecraft/auth/steps/GameEntitlementsStep.cpp | 145 + .../minecraft/auth/steps/GameEntitlementsStep.hpp | 69 + .../minecraft/auth/steps/MicrosoftOAuthStep.cpp | 319 + .../minecraft/auth/steps/MicrosoftOAuthStep.hpp | 85 + .../auth/steps/MinecraftProfileFetchStep.cpp | 170 + .../auth/steps/MinecraftProfileFetchStep.hpp | 68 + .../auth/steps/MinecraftServicesLoginStep.cpp | 138 + .../auth/steps/MinecraftServicesLoginStep.hpp | 65 + .../minecraft/auth/steps/SkinDownloadStep.cpp | 76 + .../minecraft/auth/steps/SkinDownloadStep.hpp | 63 + .../launcher/minecraft/auth/steps/Step.hpp | 128 + .../launcher/minecraft/auth/steps/Steps.hpp | 51 + .../minecraft/auth/steps/XboxLiveUserStep.cpp | 147 + .../minecraft/auth/steps/XboxLiveUserStep.hpp | 65 + .../minecraft/auth/steps/XboxProfileFetchStep.cpp | 155 + .../minecraft/auth/steps/XboxProfileFetchStep.hpp | 67 + .../minecraft/auth/steps/XboxSecurityTokenStep.cpp | 249 + .../minecraft/auth/steps/XboxSecurityTokenStep.hpp | 89 + .../launcher/minecraft/launch/AutoInstallJava.cpp | 313 + .../launcher/minecraft/launch/AutoInstallJava.hpp | 57 + .../launcher/minecraft/launch/ClaimAccount.cpp | 49 + .../launcher/minecraft/launch/ClaimAccount.hpp | 60 + .../minecraft/launch/CreateGameFolders.cpp | 46 + .../minecraft/launch/CreateGameFolders.hpp | 57 + .../launcher/minecraft/launch/ExtractNatives.cpp | 161 + .../launcher/minecraft/launch/ExtractNatives.hpp | 55 + .../minecraft/launch/LauncherPartLaunch.cpp | 275 + .../minecraft/launch/LauncherPartLaunch.hpp | 82 + .../launcher/minecraft/launch/MinecraftTarget.cpp | 120 + .../launcher/minecraft/launch/MinecraftTarget.hpp | 58 + .../launcher/minecraft/launch/ModMinecraftJar.cpp | 123 + .../launcher/minecraft/launch/ModMinecraftJar.hpp | 59 + .../minecraft/launch/PrintInstanceInfo.cpp | 110 + .../minecraft/launch/PrintInstanceInfo.hpp | 65 + .../minecraft/launch/ReconstructAssets.cpp | 57 + .../minecraft/launch/ReconstructAssets.hpp | 55 + .../launcher/minecraft/launch/ScanModFolders.cpp | 116 + .../launcher/minecraft/launch/ScanModFolders.hpp | 67 + .../minecraft/launch/VerifyJavaInstall.cpp | 80 + .../minecraft/launch/VerifyJavaInstall.hpp | 40 + .../launcher/minecraft/mod/DataPack.cpp | 267 + .../launcher/minecraft/mod/DataPack.hpp | 156 + .../launcher/minecraft/mod/DataPackFolderModel.cpp | 205 + .../launcher/minecraft/mod/DataPackFolderModel.hpp | 102 + .../launcher/minecraft/mod/MetadataHandler.hpp | 79 + .../projt-launcher/launcher/minecraft/mod/Mod.cpp | 324 + .../projt-launcher/launcher/minecraft/mod/Mod.hpp | 137 + .../launcher/minecraft/mod/ModDetails.hpp | 297 + .../launcher/minecraft/mod/ModFolderModel.cpp | 255 + .../launcher/minecraft/mod/ModFolderModel.hpp | 128 + .../launcher/minecraft/mod/Resource.cpp | 343 + .../launcher/minecraft/mod/Resource.hpp | 304 + .../launcher/minecraft/mod/ResourceFolderModel.cpp | 1094 +++ .../launcher/minecraft/mod/ResourceFolderModel.hpp | 394 + .../launcher/minecraft/mod/ResourcePack.cpp | 58 + .../launcher/minecraft/mod/ResourcePack.hpp | 48 + .../minecraft/mod/ResourcePackFolderModel.cpp | 203 + .../minecraft/mod/ResourcePackFolderModel.hpp | 66 + .../launcher/minecraft/mod/ShaderPack.cpp | 57 + .../launcher/minecraft/mod/ShaderPack.hpp | 96 + .../minecraft/mod/ShaderPackFolderModel.hpp | 56 + .../launcher/minecraft/mod/TexturePack.cpp | 105 + .../launcher/minecraft/mod/TexturePack.hpp | 100 + .../minecraft/mod/TexturePackFolderModel.cpp | 166 + .../minecraft/mod/TexturePackFolderModel.hpp | 106 + .../launcher/minecraft/mod/WorldSave.cpp | 67 + .../launcher/minecraft/mod/WorldSave.hpp | 99 + .../minecraft/mod/tasks/GetModDependenciesTask.cpp | 417 + .../minecraft/mod/tasks/GetModDependenciesTask.hpp | 126 + .../minecraft/mod/tasks/LocalDataPackParseTask.cpp | 461 + .../minecraft/mod/tasks/LocalDataPackParseTask.hpp | 115 + .../minecraft/mod/tasks/LocalModParseTask.cpp | 942 ++ .../minecraft/mod/tasks/LocalModParseTask.hpp | 93 + .../minecraft/mod/tasks/LocalResourceParse.cpp | 118 + .../minecraft/mod/tasks/LocalResourceParse.hpp | 66 + .../mod/tasks/LocalResourceUpdateTask.cpp | 99 + .../mod/tasks/LocalResourceUpdateTask.hpp | 75 + .../mod/tasks/LocalShaderPackParseTask.cpp | 141 + .../mod/tasks/LocalShaderPackParseTask.hpp | 93 + .../mod/tasks/LocalTexturePackParseTask.cpp | 306 + .../mod/tasks/LocalTexturePackParseTask.hpp | 100 + .../mod/tasks/LocalWorldSaveParseTask.cpp | 228 + .../mod/tasks/LocalWorldSaveParseTask.hpp | 93 + .../minecraft/mod/tasks/ResourceFolderLoadTask.cpp | 197 + .../minecraft/mod/tasks/ResourceFolderLoadTask.hpp | 116 + .../launcher/minecraft/skins/CapeChange.cpp | 97 + .../launcher/minecraft/skins/CapeChange.h | 62 + .../launcher/minecraft/skins/CapeListModel.cpp | 287 + .../launcher/minecraft/skins/CapeListModel.h | 137 + .../launcher/minecraft/skins/SkinDelete.cpp | 85 + .../launcher/minecraft/skins/SkinDelete.h | 59 + .../launcher/minecraft/skins/SkinList.cpp | 496 + .../launcher/minecraft/skins/SkinList.h | 115 + .../launcher/minecraft/skins/SkinModel.cpp | 211 + .../launcher/minecraft/skins/SkinModel.h | 112 + .../launcher/minecraft/skins/SkinUpload.cpp | 105 + .../launcher/minecraft/skins/SkinUpload.h | 65 + .../launcher/minecraft/update/AssetUpdateTask.cpp | 139 + .../launcher/minecraft/update/AssetUpdateTask.h | 48 + .../launcher/minecraft/update/FMLLibrariesTask.cpp | 159 + .../launcher/minecraft/update/FMLLibrariesTask.h | 50 + .../launcher/minecraft/update/FoldersTask.cpp | 79 + .../launcher/minecraft/update/FoldersTask.h | 37 + .../launcher/minecraft/update/LibrariesTask.cpp | 123 + .../launcher/minecraft/update/LibrariesTask.h | 46 + .../launcher/modplatform/CheckUpdateTask.h | 106 + .../launcher/modplatform/EnsureMetadataTask.cpp | 668 ++ .../launcher/modplatform/EnsureMetadataTask.h | 97 + .../launcher/modplatform/ModIndex.cpp | 226 + .../projt-launcher/launcher/modplatform/ModIndex.h | 352 + .../launcher/modplatform/ResourceAPI.cpp | 393 + .../launcher/modplatform/ResourceAPI.h | 189 + .../launcher/modplatform/ResourceType.cpp | 74 + .../launcher/modplatform/ResourceType.h | 79 + .../modplatform/atlauncher/ATLPackIndex.cpp | 71 + .../launcher/modplatform/atlauncher/ATLPackIndex.h | 73 + .../modplatform/atlauncher/ATLPackInstallTask.cpp | 1273 +++ .../modplatform/atlauncher/ATLPackInstallTask.h | 188 + .../modplatform/atlauncher/ATLPackManifest.cpp | 427 + .../modplatform/atlauncher/ATLPackManifest.h | 238 + .../modplatform/atlauncher/ATLShareCode.cpp | 87 + .../launcher/modplatform/atlauncher/ATLShareCode.h | 75 + .../modplatform/flame/FileResolvingTask.cpp | 389 + .../launcher/modplatform/flame/FileResolvingTask.h | 81 + .../launcher/modplatform/flame/FlameAPI.cpp | 341 + .../launcher/modplatform/flame/FlameAPI.h | 232 + .../modplatform/flame/FlameCheckUpdate.cpp | 263 + .../launcher/modplatform/flame/FlameCheckUpdate.h | 50 + .../flame/FlameInstanceCreationTask.cpp | 934 ++ .../modplatform/flame/FlameInstanceCreationTask.h | 125 + .../launcher/modplatform/flame/FlameModIndex.cpp | 239 + .../launcher/modplatform/flame/FlameModIndex.h | 36 + .../modplatform/flame/FlamePackExportTask.cpp | 566 ++ .../modplatform/flame/FlamePackExportTask.h | 116 + .../launcher/modplatform/flame/PackManifest.cpp | 95 + .../launcher/modplatform/flame/PackManifest.h | 117 + .../modplatform/helpers/ExportToModList.cpp | 261 + .../launcher/modplatform/helpers/ExportToModList.h | 68 + .../launcher/modplatform/helpers/HashUtils.cpp | 199 + .../launcher/modplatform/helpers/HashUtils.h | 90 + .../launcher/modplatform/helpers/OverrideUtils.cpp | 92 + .../launcher/modplatform/helpers/OverrideUtils.h | 41 + .../modplatform/import_ftb/PackHelpers.cpp | 180 + .../launcher/modplatform/import_ftb/PackHelpers.h | 82 + .../modplatform/import_ftb/PackInstallTask.cpp | 146 + .../modplatform/import_ftb/PackInstallTask.h | 76 + .../modplatform/legacy_ftb/PackFetchTask.cpp | 258 + .../modplatform/legacy_ftb/PackFetchTask.h | 69 + .../launcher/modplatform/legacy_ftb/PackHelpers.h | 66 + .../modplatform/legacy_ftb/PackInstallTask.cpp | 283 + .../modplatform/legacy_ftb/PackInstallTask.h | 82 + .../modplatform/legacy_ftb/PrivatePackManager.cpp | 103 + .../modplatform/legacy_ftb/PrivatePackManager.h | 64 + .../launcher/modplatform/modrinth/ModrinthAPI.cpp | 207 + .../launcher/modplatform/modrinth/ModrinthAPI.h | 291 + .../modplatform/modrinth/ModrinthCheckUpdate.cpp | 291 + .../modplatform/modrinth/ModrinthCheckUpdate.h | 51 + .../modrinth/ModrinthCollectionImportTask.cpp | 355 + .../modrinth/ModrinthCollectionImportTask.h | 98 + .../modrinth/ModrinthInstanceCreationTask.cpp | 668 ++ .../modrinth/ModrinthInstanceCreationTask.h | 104 + .../modrinth/ModrinthPackExportTask.cpp | 386 + .../modplatform/modrinth/ModrinthPackExportTask.h | 103 + .../modplatform/modrinth/ModrinthPackIndex.cpp | 284 + .../modplatform/modrinth/ModrinthPackIndex.h | 56 + .../launcher/modplatform/packwiz/Packwiz.cpp | 494 + .../launcher/modplatform/packwiz/Packwiz.h | 145 + .../technic/SingleZipPackInstallTask.cpp | 185 + .../modplatform/technic/SingleZipPackInstallTask.h | 89 + .../modplatform/technic/SolderPackInstallTask.cpp | 276 + .../modplatform/technic/SolderPackInstallTask.h | 116 + .../modplatform/technic/SolderPackManifest.cpp | 85 + .../modplatform/technic/SolderPackManifest.h | 77 + .../modplatform/technic/TechnicPackProcessor.cpp | 277 + .../modplatform/technic/TechnicPackProcessor.h | 62 + .../projt-launcher/launcher/net/ApiDownload.cpp | 70 + archived/projt-launcher/launcher/net/ApiDownload.h | 59 + .../projt-launcher/launcher/net/ApiHeaderProxy.h | 79 + archived/projt-launcher/launcher/net/ApiUpload.cpp | 56 + archived/projt-launcher/launcher/net/ApiUpload.h | 55 + .../projt-launcher/launcher/net/ByteArraySink.h | 126 + .../launcher/net/ChecksumValidator.h | 123 + archived/projt-launcher/launcher/net/Download.cpp | 115 + archived/projt-launcher/launcher/net/Download.h | 92 + archived/projt-launcher/launcher/net/FileSink.cpp | 181 + archived/projt-launcher/launcher/net/FileSink.h | 90 + archived/projt-launcher/launcher/net/HeaderProxy.h | 78 + .../projt-launcher/launcher/net/HttpMetaCache.cpp | 403 + .../projt-launcher/launcher/net/HttpMetaCache.h | 216 + archived/projt-launcher/launcher/net/Logging.cpp | 50 + archived/projt-launcher/launcher/net/Logging.h | 52 + .../projt-launcher/launcher/net/MetaCacheSink.cpp | 182 + .../projt-launcher/launcher/net/MetaCacheSink.h | 85 + archived/projt-launcher/launcher/net/Mode.h | 30 + archived/projt-launcher/launcher/net/NetJob.cpp | 231 + archived/projt-launcher/launcher/net/NetJob.h | 110 + .../projt-launcher/launcher/net/NetRequest.cpp | 418 + archived/projt-launcher/launcher/net/NetRequest.h | 158 + archived/projt-launcher/launcher/net/NetUtils.h | 68 + .../projt-launcher/launcher/net/PasteUpload.cpp | 280 + archived/projt-launcher/launcher/net/PasteUpload.h | 127 + .../projt-launcher/launcher/net/RawHeaderProxy.h | 86 + archived/projt-launcher/launcher/net/Sink.h | 136 + archived/projt-launcher/launcher/net/Upload.cpp | 87 + archived/projt-launcher/launcher/net/Upload.h | 86 + archived/projt-launcher/launcher/net/Validator.h | 79 + .../projt-launcher/launcher/news/NewsChecker.cpp | 181 + .../projt-launcher/launcher/news/NewsChecker.h | 126 + .../projt-launcher/launcher/news/NewsEntry.cpp | 103 + archived/projt-launcher/launcher/news/NewsEntry.h | 77 + archived/projt-launcher/launcher/qtlogging.ini | 19 + .../projt-launcher/launcher/resources/OSX/OSX.qrc | 43 + .../launcher/resources/OSX/index.theme | 11 + .../launcher/resources/OSX/scalable/about.svg | 20 + .../launcher/resources/OSX/scalable/accounts.svg | 16 + .../launcher/resources/OSX/scalable/bug.svg | 25 + .../resources/OSX/scalable/centralmods.svg | 16 + .../resources/OSX/scalable/checkupdate.svg | 22 + .../launcher/resources/OSX/scalable/copy.svg | 18 + .../launcher/resources/OSX/scalable/coremods.svg | 21 + .../resources/OSX/scalable/custom-commands.svg | 71 + .../launcher/resources/OSX/scalable/delete.svg | 49 + .../launcher/resources/OSX/scalable/export.svg | 65 + .../resources/OSX/scalable/externaltools.svg | 14 + .../launcher/resources/OSX/scalable/help.svg | 51 + .../resources/OSX/scalable/instance-settings.svg | 25 + .../launcher/resources/OSX/scalable/jarmods.svg | 30 + .../launcher/resources/OSX/scalable/java.svg | 33 + .../launcher/resources/OSX/scalable/language.svg | 40 + .../launcher/resources/OSX/scalable/launch.svg | 33 + .../launcher/resources/OSX/scalable/loadermods.svg | 14 + .../launcher/resources/OSX/scalable/log.svg | 19 + .../launcher/resources/OSX/scalable/minecraft.svg | 12 + .../launcher/resources/OSX/scalable/new.svg | 19 + .../launcher/resources/OSX/scalable/news.svg | 16 + .../launcher/resources/OSX/scalable/notes.svg | 21 + .../launcher/resources/OSX/scalable/patreon.svg | 15 + .../launcher/resources/OSX/scalable/proxy.svg | 16 + .../launcher/resources/OSX/scalable/refresh.svg | 16 + .../launcher/resources/OSX/scalable/rename.svg | 27 + .../resources/OSX/scalable/resourcepacks.svg | 17 + .../resources/OSX/scalable/screenshots.svg | 19 + .../launcher/resources/OSX/scalable/settings.svg | 25 + .../resources/OSX/scalable/shaderpacks.svg | 46 + .../launcher/resources/OSX/scalable/shortcut.svg | 14 + .../launcher/resources/OSX/scalable/status-bad.svg | 11 + .../resources/OSX/scalable/status-good.svg | 19 + .../resources/OSX/scalable/status-yellow.svg | 16 + .../launcher/resources/OSX/scalable/tag.svg | 35 + .../launcher/resources/OSX/scalable/viewfolder.svg | 16 + .../launcher/resources/OSX/scalable/worlds.svg | 58 + .../resources/assets/underconstruction.png | Bin 0 -> 12372 bytes .../launcher/resources/backgrounds/backgrounds.qrc | 29 + .../launcher/resources/backgrounds/kitteh-bday.png | Bin 0 -> 65715 bytes .../resources/backgrounds/kitteh-spooky.png | Bin 0 -> 76354 bytes .../launcher/resources/backgrounds/kitteh-xmas.png | Bin 0 -> 68931 bytes .../launcher/resources/backgrounds/kitteh.png | Bin 0 -> 58885 bytes .../launcher/resources/backgrounds/rory-bday.png | Bin 0 -> 75145 bytes .../resources/backgrounds/rory-flat-bday.png | Bin 0 -> 47977 bytes .../resources/backgrounds/rory-flat-spooky.png | Bin 0 -> 47832 bytes .../resources/backgrounds/rory-flat-xmas.png | Bin 0 -> 48992 bytes .../launcher/resources/backgrounds/rory-flat.png | Bin 0 -> 43052 bytes .../launcher/resources/backgrounds/rory-spooky.png | Bin 0 -> 76908 bytes .../launcher/resources/backgrounds/rory-xmas.png | Bin 0 -> 76541 bytes .../launcher/resources/backgrounds/rory.png | Bin 0 -> 68309 bytes .../launcher/resources/backgrounds/teawie-bday.png | Bin 0 -> 161203 bytes .../resources/backgrounds/teawie-spooky.png | Bin 0 -> 173134 bytes .../launcher/resources/backgrounds/teawie-xmas.png | Bin 0 -> 168267 bytes .../launcher/resources/backgrounds/teawie.png | Bin 0 -> 159697 bytes .../launcher/resources/breeze_dark/breeze_dark.qrc | 48 + .../launcher/resources/breeze_dark/index.theme | 11 + .../resources/breeze_dark/scalable/about.svg | 12 + .../resources/breeze_dark/scalable/accounts.svg | 17 + .../resources/breeze_dark/scalable/appearance.svg | 13 + .../resources/breeze_dark/scalable/bug.svg | 13 + .../resources/breeze_dark/scalable/centralmods.svg | 1 + .../resources/breeze_dark/scalable/checkupdate.svg | 14 + .../resources/breeze_dark/scalable/copy.svg | 11 + .../resources/breeze_dark/scalable/coremods.svg | 1 + .../breeze_dark/scalable/custom-commands.svg | 13 + .../resources/breeze_dark/scalable/datapacks.svg | 13 + .../resources/breeze_dark/scalable/delete.svg | 13 + .../resources/breeze_dark/scalable/discord.svg | 1 + .../resources/breeze_dark/scalable/export.svg | 11 + .../breeze_dark/scalable/externaltools.svg | 13 + .../resources/breeze_dark/scalable/help.svg | 13 + .../breeze_dark/scalable/instance-settings.svg | 13 + .../resources/breeze_dark/scalable/jarmods.svg | 1 + .../resources/breeze_dark/scalable/java.svg | 10 + .../resources/breeze_dark/scalable/language.svg | 13 + .../resources/breeze_dark/scalable/launch.svg | 8 + .../resources/breeze_dark/scalable/loadermods.svg | 13 + .../resources/breeze_dark/scalable/log.svg | 13 + .../resources/breeze_dark/scalable/matrix.svg | 9 + .../resources/breeze_dark/scalable/minecraft.svg | 13 + .../resources/breeze_dark/scalable/new.svg | 13 + .../resources/breeze_dark/scalable/news.svg | 13 + .../resources/breeze_dark/scalable/notes.svg | 13 + .../resources/breeze_dark/scalable/patreon.svg | 3 + .../resources/breeze_dark/scalable/proxy.svg | 14 + .../breeze_dark/scalable/reddit-alien.svg | 3 + .../resources/breeze_dark/scalable/refresh.svg | 8 + .../resources/breeze_dark/scalable/rename.svg | 13 + .../breeze_dark/scalable/resourcepacks.svg | 11 + .../resources/breeze_dark/scalable/screenshots.svg | 13 + .../resources/breeze_dark/scalable/server.svg | 13 + .../resources/breeze_dark/scalable/settings.svg | 17 + .../resources/breeze_dark/scalable/shaderpacks.svg | 13 + .../resources/breeze_dark/scalable/shortcut.svg | 18 + .../resources/breeze_dark/scalable/status-bad.svg | 9 + .../resources/breeze_dark/scalable/status-good.svg | 10 + .../breeze_dark/scalable/status-yellow.svg | 9 + .../resources/breeze_dark/scalable/tag.svg | 17 + .../resources/breeze_dark/scalable/viewfolder.svg | 13 + .../resources/breeze_dark/scalable/worlds.svg | 16 + .../resources/breeze_light/breeze_light.qrc | 48 + .../launcher/resources/breeze_light/index.theme | 11 + .../resources/breeze_light/scalable/about.svg | 12 + .../resources/breeze_light/scalable/accounts.svg | 17 + .../resources/breeze_light/scalable/appearance.svg | 13 + .../resources/breeze_light/scalable/bug.svg | 13 + .../breeze_light/scalable/centralmods.svg | 1 + .../breeze_light/scalable/checkupdate.svg | 14 + .../resources/breeze_light/scalable/copy.svg | 11 + .../resources/breeze_light/scalable/coremods.svg | 1 + .../breeze_light/scalable/custom-commands.svg | 13 + .../resources/breeze_light/scalable/datapacks.svg | 13 + .../resources/breeze_light/scalable/delete.svg | 13 + .../resources/breeze_light/scalable/discord.svg | 1 + .../resources/breeze_light/scalable/export.svg | 11 + .../breeze_light/scalable/externaltools.svg | 13 + .../resources/breeze_light/scalable/help.svg | 13 + .../breeze_light/scalable/instance-settings.svg | 13 + .../resources/breeze_light/scalable/jarmods.svg | 1 + .../resources/breeze_light/scalable/java.svg | 10 + .../resources/breeze_light/scalable/language.svg | 13 + .../resources/breeze_light/scalable/launch.svg | 8 + .../resources/breeze_light/scalable/loadermods.svg | 13 + .../resources/breeze_light/scalable/log.svg | 13 + .../resources/breeze_light/scalable/matrix.svg | 9 + .../resources/breeze_light/scalable/minecraft.svg | 13 + .../resources/breeze_light/scalable/new.svg | 13 + .../resources/breeze_light/scalable/news.svg | 13 + .../resources/breeze_light/scalable/notes.svg | 13 + .../resources/breeze_light/scalable/patreon.svg | 3 + .../resources/breeze_light/scalable/proxy.svg | 14 + .../breeze_light/scalable/reddit-alien.svg | 3 + .../resources/breeze_light/scalable/refresh.svg | 8 + .../resources/breeze_light/scalable/rename.svg | 13 + .../breeze_light/scalable/resourcepacks.svg | 11 + .../breeze_light/scalable/screenshots.svg | 13 + .../resources/breeze_light/scalable/server.svg | 13 + .../resources/breeze_light/scalable/settings.svg | 17 + .../breeze_light/scalable/shaderpacks.svg | 13 + .../resources/breeze_light/scalable/shortcut.svg | 18 + .../resources/breeze_light/scalable/status-bad.svg | 9 + .../breeze_light/scalable/status-good.svg | 10 + .../breeze_light/scalable/status-yellow.svg | 9 + .../resources/breeze_light/scalable/tag.svg | 17 + .../resources/breeze_light/scalable/viewfolder.svg | 13 + .../resources/breeze_light/scalable/worlds.svg | 16 + .../launcher/resources/documents/credits.html | 45 + .../launcher/resources/documents/documents.qrc | 8 + .../launcher/resources/documents/manifesto.md | 108 + .../launcher/resources/flat/flat.qrc | 54 + .../launcher/resources/flat/index.theme | 11 + .../launcher/resources/flat/scalable/about.svg | 3 + .../launcher/resources/flat/scalable/accounts.svg | 3 + .../resources/flat/scalable/appearance.svg | 1 + .../launcher/resources/flat/scalable/bug.svg | 3 + .../launcher/resources/flat/scalable/cat.svg | 3 + .../resources/flat/scalable/centralmods.svg | 3 + .../resources/flat/scalable/checkupdate.svg | 3 + .../launcher/resources/flat/scalable/copy.svg | 3 + .../launcher/resources/flat/scalable/coremods.svg | 3 + .../resources/flat/scalable/custom-commands.svg | 1 + .../launcher/resources/flat/scalable/datapacks.svg | 86 + .../launcher/resources/flat/scalable/delete.svg | 1 + .../launcher/resources/flat/scalable/discord.svg | 4 + .../launcher/resources/flat/scalable/export.svg | 1 + .../resources/flat/scalable/externaltools.svg | 3 + .../launcher/resources/flat/scalable/help.svg | 17 + .../resources/flat/scalable/instance-settings.svg | 3 + .../launcher/resources/flat/scalable/jarmods.svg | 3 + .../launcher/resources/flat/scalable/java.svg | 3 + .../launcher/resources/flat/scalable/language.svg | 103 + .../launcher/resources/flat/scalable/launch.svg | 16 + .../resources/flat/scalable/loadermods.svg | 3 + .../launcher/resources/flat/scalable/log.svg | 3 + .../launcher/resources/flat/scalable/minecraft.svg | 3 + .../launcher/resources/flat/scalable/multimc.svg | 3 + .../launcher/resources/flat/scalable/new.svg | 3 + .../launcher/resources/flat/scalable/news.svg | 3 + .../launcher/resources/flat/scalable/notes.svg | 3 + .../launcher/resources/flat/scalable/packages.svg | 3 + .../launcher/resources/flat/scalable/patreon.svg | 3 + .../launcher/resources/flat/scalable/proxy.svg | 3 + .../launcher/resources/flat/scalable/quickmods.svg | 3 + .../resources/flat/scalable/reddit-alien.svg | 3 + .../launcher/resources/flat/scalable/refresh.svg | 3 + .../launcher/resources/flat/scalable/rename.svg | 1 + .../resources/flat/scalable/resourcepacks.svg | 3 + .../flat/scalable/screenshot-placeholder.svg | 3 + .../resources/flat/scalable/screenshots.svg | 3 + .../launcher/resources/flat/scalable/server.svg | 44 + .../launcher/resources/flat/scalable/settings.svg | 3 + .../resources/flat/scalable/shaderpacks.svg | 56 + .../launcher/resources/flat/scalable/shortcut.svg | 3 + .../launcher/resources/flat/scalable/star.svg | 3 + .../resources/flat/scalable/status-bad.svg | 3 + .../resources/flat/scalable/status-good.svg | 3 + .../resources/flat/scalable/status-running.svg | 3 + .../resources/flat/scalable/status-yellow.svg | 3 + .../launcher/resources/flat/scalable/tag.svg | 1 + .../resources/flat/scalable/viewfolder.svg | 3 + .../launcher/resources/flat/scalable/worlds.svg | 3 + .../launcher/resources/flat_white/flat_white.qrc | 54 + .../launcher/resources/flat_white/index.theme | 11 + .../resources/flat_white/scalable/about.svg | 3 + .../resources/flat_white/scalable/accounts.svg | 3 + .../resources/flat_white/scalable/appearance.svg | 1 + .../launcher/resources/flat_white/scalable/bug.svg | 3 + .../launcher/resources/flat_white/scalable/cat.svg | 3 + .../resources/flat_white/scalable/centralmods.svg | 3 + .../resources/flat_white/scalable/checkupdate.svg | 3 + .../resources/flat_white/scalable/copy.svg | 3 + .../resources/flat_white/scalable/coremods.svg | 3 + .../flat_white/scalable/custom-commands.svg | 1 + .../resources/flat_white/scalable/datapacks.svg | 86 + .../resources/flat_white/scalable/delete.svg | 5 + .../resources/flat_white/scalable/discord.svg | 4 + .../resources/flat_white/scalable/export.svg | 5 + .../flat_white/scalable/externaltools.svg | 3 + .../resources/flat_white/scalable/help.svg | 17 + .../flat_white/scalable/instance-settings.svg | 3 + .../resources/flat_white/scalable/jarmods.svg | 3 + .../resources/flat_white/scalable/java.svg | 3 + .../resources/flat_white/scalable/language.svg | 103 + .../resources/flat_white/scalable/launch.svg | 16 + .../resources/flat_white/scalable/loadermods.svg | 3 + .../launcher/resources/flat_white/scalable/log.svg | 3 + .../resources/flat_white/scalable/minecraft.svg | 3 + .../resources/flat_white/scalable/multimc.svg | 3 + .../launcher/resources/flat_white/scalable/new.svg | 3 + .../resources/flat_white/scalable/news.svg | 3 + .../resources/flat_white/scalable/notes.svg | 3 + .../resources/flat_white/scalable/packages.svg | 3 + .../resources/flat_white/scalable/patreon.svg | 3 + .../resources/flat_white/scalable/proxy.svg | 3 + .../resources/flat_white/scalable/quickmods.svg | 3 + .../resources/flat_white/scalable/reddit-alien.svg | 3 + .../resources/flat_white/scalable/refresh.svg | 3 + .../resources/flat_white/scalable/rename.svg | 4 + .../flat_white/scalable/resourcepacks.svg | 3 + .../flat_white/scalable/screenshot-placeholder.svg | 3 + .../resources/flat_white/scalable/screenshots.svg | 3 + .../resources/flat_white/scalable/server.svg | 1 + .../resources/flat_white/scalable/settings.svg | 3 + .../resources/flat_white/scalable/shaderpacks.svg | 56 + .../resources/flat_white/scalable/shortcut.svg | 3 + .../resources/flat_white/scalable/star.svg | 3 + .../resources/flat_white/scalable/status-bad.svg | 3 + .../resources/flat_white/scalable/status-good.svg | 3 + .../flat_white/scalable/status-running.svg | 3 + .../flat_white/scalable/status-yellow.svg | 3 + .../launcher/resources/flat_white/scalable/tag.svg | 4 + .../resources/flat_white/scalable/viewfolder.svg | 3 + .../resources/flat_white/scalable/worlds.svg | 3 + .../projt-launcher/launcher/resources/iOS/iOS.qrc | 43 + .../launcher/resources/iOS/index.theme | 11 + .../launcher/resources/iOS/scalable/about.svg | 16 + .../launcher/resources/iOS/scalable/accounts.svg | 14 + .../launcher/resources/iOS/scalable/bug.svg | 22 + .../resources/iOS/scalable/centralmods.svg | 13 + .../resources/iOS/scalable/checkupdate.svg | 16 + .../launcher/resources/iOS/scalable/copy.svg | 13 + .../launcher/resources/iOS/scalable/coremods.svg | 18 + .../resources/iOS/scalable/custom-commands.svg | 63 + .../launcher/resources/iOS/scalable/delete.svg | 31 + .../launcher/resources/iOS/scalable/export.svg | 34 + .../resources/iOS/scalable/externaltools.svg | 13 + .../launcher/resources/iOS/scalable/help.svg | 38 + .../resources/iOS/scalable/instance-settings.svg | 19 + .../launcher/resources/iOS/scalable/jarmods.svg | 31 + .../launcher/resources/iOS/scalable/java.svg | 33 + .../launcher/resources/iOS/scalable/language.svg | 32 + .../launcher/resources/iOS/scalable/launch.svg | 17 + .../launcher/resources/iOS/scalable/loadermods.svg | 14 + .../launcher/resources/iOS/scalable/log.svg | 13 + .../launcher/resources/iOS/scalable/minecraft.svg | 8 + .../launcher/resources/iOS/scalable/multimc.svg | 13 + .../launcher/resources/iOS/scalable/new.svg | 13 + .../launcher/resources/iOS/scalable/news.svg | 14 + .../launcher/resources/iOS/scalable/notes.svg | 15 + .../launcher/resources/iOS/scalable/patreon.svg | 12 + .../launcher/resources/iOS/scalable/proxy.svg | 11 + .../launcher/resources/iOS/scalable/refresh.svg | 13 + .../launcher/resources/iOS/scalable/rename.svg | 16 + .../resources/iOS/scalable/resourcepacks.svg | 15 + .../resources/iOS/scalable/screenshots.svg | 14 + .../launcher/resources/iOS/scalable/settings.svg | 19 + .../resources/iOS/scalable/shaderpacks.svg | 38 + .../launcher/resources/iOS/scalable/shortcut.svg | 13 + .../launcher/resources/iOS/scalable/status-bad.svg | 10 + .../resources/iOS/scalable/status-good.svg | 18 + .../resources/iOS/scalable/status-yellow.svg | 56 + .../launcher/resources/iOS/scalable/tag.svg | 20 + .../launcher/resources/iOS/scalable/viewfolder.svg | 12 + .../launcher/resources/iOS/scalable/worlds.svg | 44 + .../multimc/128x128/instances/chicken_legacy.png | Bin 0 -> 5178 bytes .../multimc/128x128/instances/creeper_legacy.png | Bin 0 -> 7449 bytes .../128x128/instances/enderpearl_legacy.png | Bin 0 -> 18345 bytes .../multimc/128x128/instances/flame_legacy.png | Bin 0 -> 2888 bytes .../resources/multimc/128x128/instances/forge.png | Bin 0 -> 1552 bytes .../multimc/128x128/instances/ftb_glow.png | Bin 0 -> 11312 bytes .../multimc/128x128/instances/ftb_logo_legacy.png | Bin 0 -> 6664 bytes .../multimc/128x128/instances/gear_legacy.png | Bin 0 -> 16940 bytes .../multimc/128x128/instances/herobrine_legacy.png | Bin 0 -> 4360 bytes .../multimc/128x128/instances/infinity_legacy.png | Bin 0 -> 6106 bytes .../multimc/128x128/instances/liteloader.png | Bin 0 -> 5579 bytes .../multimc/128x128/instances/magitech_legacy.png | Bin 0 -> 21807 bytes .../multimc/128x128/instances/meat_legacy.png | Bin 0 -> 7876 bytes .../128x128/instances/netherstar_legacy.png | Bin 0 -> 10037 bytes .../multimc/128x128/instances/skeleton_legacy.png | Bin 0 -> 2882 bytes .../128x128/instances/squarecreeper_legacy.png | Bin 0 -> 7099 bytes .../multimc/128x128/instances/steve_legacy.png | Bin 0 -> 3640 bytes .../resources/multimc/128x128/shaderpacks.png | Bin 0 -> 10629 bytes .../resources/multimc/128x128/unknown_server.png | Bin 0 -> 6726 bytes .../launcher/resources/multimc/16x16/bug.png | Bin 0 -> 588 bytes .../launcher/resources/multimc/16x16/cat.png | Bin 0 -> 509 bytes .../resources/multimc/16x16/centralmods.png | Bin 0 -> 557 bytes .../launcher/resources/multimc/16x16/copy.png | Bin 0 -> 396 bytes .../launcher/resources/multimc/16x16/coremods.png | Bin 0 -> 654 bytes .../launcher/resources/multimc/16x16/help.png | Bin 0 -> 779 bytes .../resources/multimc/16x16/instance-settings.png | Bin 0 -> 829 bytes .../launcher/resources/multimc/16x16/jarmods.png | Bin 0 -> 585 bytes .../resources/multimc/16x16/loadermods.png | Bin 0 -> 609 bytes .../launcher/resources/multimc/16x16/log.png | Bin 0 -> 312 bytes .../launcher/resources/multimc/16x16/minecraft.png | Bin 0 -> 741 bytes .../launcher/resources/multimc/16x16/new.png | Bin 0 -> 625 bytes .../launcher/resources/multimc/16x16/news.png | Bin 0 -> 598 bytes .../launcher/resources/multimc/16x16/noaccount.png | Bin 0 -> 174 bytes .../launcher/resources/multimc/16x16/patreon.png | Bin 0 -> 278 bytes .../launcher/resources/multimc/16x16/refresh.png | Bin 0 -> 799 bytes .../resources/multimc/16x16/resourcepacks.png | Bin 0 -> 906 bytes .../resources/multimc/16x16/screenshots.png | Bin 0 -> 533 bytes .../launcher/resources/multimc/16x16/settings.png | Bin 0 -> 829 bytes .../launcher/resources/multimc/16x16/star.png | Bin 0 -> 628 bytes .../resources/multimc/16x16/status-bad.png | Bin 0 -> 400 bytes .../resources/multimc/16x16/status-good.png | Bin 0 -> 483 bytes .../resources/multimc/16x16/status-running.png | Bin 0 -> 475 bytes .../resources/multimc/16x16/status-yellow.png | Bin 0 -> 398 bytes .../resources/multimc/16x16/viewfolder.png | Bin 0 -> 319 bytes .../launcher/resources/multimc/16x16/worlds.png | Bin 0 -> 768 bytes .../launcher/resources/multimc/22x22/bug.png | Bin 0 -> 966 bytes .../launcher/resources/multimc/22x22/cat.png | Bin 0 -> 713 bytes .../resources/multimc/22x22/centralmods.png | Bin 0 -> 984 bytes .../launcher/resources/multimc/22x22/copy.png | Bin 0 -> 441 bytes .../launcher/resources/multimc/22x22/help.png | Bin 0 -> 1204 bytes .../resources/multimc/22x22/instance-settings.png | Bin 0 -> 1283 bytes .../launcher/resources/multimc/22x22/news.png | Bin 0 -> 932 bytes .../launcher/resources/multimc/22x22/patreon.png | Bin 0 -> 332 bytes .../launcher/resources/multimc/22x22/refresh.png | Bin 0 -> 1122 bytes .../resources/multimc/22x22/screenshots.png | Bin 0 -> 849 bytes .../launcher/resources/multimc/22x22/settings.png | Bin 0 -> 1283 bytes .../resources/multimc/22x22/status-bad.png | Bin 0 -> 561 bytes .../resources/multimc/22x22/status-good.png | Bin 0 -> 724 bytes .../resources/multimc/22x22/status-running.png | Bin 0 -> 702 bytes .../resources/multimc/22x22/status-yellow.png | Bin 0 -> 592 bytes .../resources/multimc/22x22/viewfolder.png | Bin 0 -> 463 bytes .../launcher/resources/multimc/22x22/worlds.png | Bin 0 -> 1262 bytes .../launcher/resources/multimc/24x24/cat.png | Bin 0 -> 809 bytes .../launcher/resources/multimc/24x24/coremods.png | Bin 0 -> 1173 bytes .../launcher/resources/multimc/24x24/jarmods.png | Bin 0 -> 1024 bytes .../resources/multimc/24x24/loadermods.png | Bin 0 -> 1092 bytes .../launcher/resources/multimc/24x24/log.png | Bin 0 -> 582 bytes .../launcher/resources/multimc/24x24/minecraft.png | Bin 0 -> 1425 bytes .../launcher/resources/multimc/24x24/noaccount.png | Bin 0 -> 186 bytes .../launcher/resources/multimc/24x24/patreon.png | Bin 0 -> 377 bytes .../resources/multimc/24x24/resourcepacks.png | Bin 0 -> 1635 bytes .../launcher/resources/multimc/24x24/star.png | Bin 0 -> 1062 bytes .../resources/multimc/24x24/status-bad.png | Bin 0 -> 632 bytes .../resources/multimc/24x24/status-good.png | Bin 0 -> 794 bytes .../resources/multimc/24x24/status-running.png | Bin 0 -> 749 bytes .../resources/multimc/24x24/status-yellow.png | Bin 0 -> 648 bytes .../resources/multimc/256x256/minecraft.png | Bin 0 -> 42727 bytes .../launcher/resources/multimc/32x32/bug.png | Bin 0 -> 1570 bytes .../launcher/resources/multimc/32x32/cat.png | Bin 0 -> 1126 bytes .../resources/multimc/32x32/centralmods.png | Bin 0 -> 1477 bytes .../launcher/resources/multimc/32x32/copy.png | Bin 0 -> 777 bytes .../launcher/resources/multimc/32x32/coremods.png | Bin 0 -> 1595 bytes .../launcher/resources/multimc/32x32/help.png | Bin 0 -> 2161 bytes .../resources/multimc/32x32/instance-settings.png | Bin 0 -> 2230 bytes .../multimc/32x32/instances/brick_legacy.png | Bin 0 -> 231 bytes .../multimc/32x32/instances/chicken_legacy.png | Bin 0 -> 962 bytes .../multimc/32x32/instances/creeper_legacy.png | Bin 0 -> 1361 bytes .../multimc/32x32/instances/diamond_legacy.png | Bin 0 -> 228 bytes .../multimc/32x32/instances/dirt_legacy.png | Bin 0 -> 211 bytes .../multimc/32x32/instances/enderpearl_legacy.png | Bin 0 -> 1893 bytes .../resources/multimc/32x32/instances/ftb_glow.png | Bin 0 -> 1515 bytes .../multimc/32x32/instances/ftb_logo_legacy.png | Bin 0 -> 1403 bytes .../multimc/32x32/instances/gear_legacy.png | Bin 0 -> 2270 bytes .../multimc/32x32/instances/gold_legacy.png | Bin 0 -> 228 bytes .../multimc/32x32/instances/grass_legacy.png | Bin 0 -> 405 bytes .../multimc/32x32/instances/herobrine_legacy.png | Bin 0 -> 850 bytes .../multimc/32x32/instances/infinity_legacy.png | Bin 0 -> 1319 bytes .../multimc/32x32/instances/iron_legacy.png | Bin 0 -> 192 bytes .../multimc/32x32/instances/magitech_legacy.png | Bin 0 -> 2503 bytes .../multimc/32x32/instances/meat_legacy.png | Bin 0 -> 1129 bytes .../multimc/32x32/instances/netherstar_legacy.png | Bin 0 -> 1201 bytes .../multimc/32x32/instances/planks_legacy.png | Bin 0 -> 226 bytes .../multimc/32x32/instances/skeleton_legacy.png | Bin 0 -> 442 bytes .../32x32/instances/squarecreeper_legacy.png | Bin 0 -> 1388 bytes .../multimc/32x32/instances/steve_legacy.png | Bin 0 -> 757 bytes .../multimc/32x32/instances/stone_legacy.png | Bin 0 -> 185 bytes .../multimc/32x32/instances/tnt_legacy.png | Bin 0 -> 228 bytes .../launcher/resources/multimc/32x32/jarmods.png | Bin 0 -> 1406 bytes .../resources/multimc/32x32/loadermods.png | Bin 0 -> 1539 bytes .../launcher/resources/multimc/32x32/log.png | Bin 0 -> 783 bytes .../launcher/resources/multimc/32x32/minecraft.png | Bin 0 -> 2308 bytes .../launcher/resources/multimc/32x32/news.png | Bin 0 -> 1342 bytes .../launcher/resources/multimc/32x32/noaccount.png | Bin 0 -> 182 bytes .../launcher/resources/multimc/32x32/patreon.png | Bin 0 -> 441 bytes .../launcher/resources/multimc/32x32/refresh.png | Bin 0 -> 1919 bytes .../resources/multimc/32x32/resourcepacks.png | Bin 0 -> 2363 bytes .../resources/multimc/32x32/screenshots.png | Bin 0 -> 1387 bytes .../launcher/resources/multimc/32x32/settings.png | Bin 0 -> 2230 bytes .../launcher/resources/multimc/32x32/star.png | Bin 0 -> 1460 bytes .../resources/multimc/32x32/status-bad.png | Bin 0 -> 876 bytes .../resources/multimc/32x32/status-good.png | Bin 0 -> 1023 bytes .../resources/multimc/32x32/status-running.png | Bin 0 -> 1045 bytes .../resources/multimc/32x32/status-yellow.png | Bin 0 -> 907 bytes .../resources/multimc/32x32/viewfolder.png | Bin 0 -> 827 bytes .../launcher/resources/multimc/32x32/worlds.png | Bin 0 -> 2144 bytes .../launcher/resources/multimc/48x48/bug.png | Bin 0 -> 2680 bytes .../launcher/resources/multimc/48x48/cat.png | Bin 0 -> 1928 bytes .../resources/multimc/48x48/centralmods.png | Bin 0 -> 2497 bytes .../launcher/resources/multimc/48x48/copy.png | Bin 0 -> 1262 bytes .../launcher/resources/multimc/48x48/help.png | Bin 0 -> 3527 bytes .../resources/multimc/48x48/instance-settings.png | Bin 0 -> 4146 bytes .../launcher/resources/multimc/48x48/log.png | Bin 0 -> 1696 bytes .../launcher/resources/multimc/48x48/minecraft.png | Bin 0 -> 4526 bytes .../launcher/resources/multimc/48x48/news.png | Bin 0 -> 2408 bytes .../launcher/resources/multimc/48x48/noaccount.png | Bin 0 -> 183 bytes .../launcher/resources/multimc/48x48/patreon.png | Bin 0 -> 622 bytes .../launcher/resources/multimc/48x48/refresh.png | Bin 0 -> 3402 bytes .../resources/multimc/48x48/screenshots.png | Bin 0 -> 2469 bytes .../launcher/resources/multimc/48x48/settings.png | Bin 0 -> 4146 bytes .../launcher/resources/multimc/48x48/star.png | Bin 0 -> 2729 bytes .../resources/multimc/48x48/status-bad.png | Bin 0 -> 1509 bytes .../resources/multimc/48x48/status-good.png | Bin 0 -> 1769 bytes .../resources/multimc/48x48/status-running.png | Bin 0 -> 1732 bytes .../resources/multimc/48x48/status-yellow.png | Bin 0 -> 1491 bytes .../resources/multimc/48x48/viewfolder.png | Bin 0 -> 1247 bytes .../launcher/resources/multimc/48x48/worlds.png | Bin 0 -> 3726 bytes .../multimc/50x50/instances/enderman_legacy.png | Bin 0 -> 1246 bytes .../launcher/resources/multimc/64x64/bug.png | Bin 0 -> 3810 bytes .../launcher/resources/multimc/64x64/cat.png | Bin 0 -> 2963 bytes .../resources/multimc/64x64/centralmods.png | Bin 0 -> 3607 bytes .../launcher/resources/multimc/64x64/copy.png | Bin 0 -> 2072 bytes .../launcher/resources/multimc/64x64/coremods.png | Bin 0 -> 4218 bytes .../launcher/resources/multimc/64x64/help.png | Bin 0 -> 4351 bytes .../resources/multimc/64x64/instance-settings.png | Bin 0 -> 6346 bytes .../launcher/resources/multimc/64x64/jarmods.png | Bin 0 -> 3618 bytes .../resources/multimc/64x64/loadermods.png | Bin 0 -> 3894 bytes .../launcher/resources/multimc/64x64/log.png | Bin 0 -> 2265 bytes .../launcher/resources/multimc/64x64/news.png | Bin 0 -> 3446 bytes .../launcher/resources/multimc/64x64/patreon.png | Bin 0 -> 785 bytes .../launcher/resources/multimc/64x64/refresh.png | Bin 0 -> 5103 bytes .../resources/multimc/64x64/resourcepacks.png | Bin 0 -> 5926 bytes .../resources/multimc/64x64/screenshots.png | Bin 0 -> 3864 bytes .../launcher/resources/multimc/64x64/settings.png | Bin 0 -> 6346 bytes .../launcher/resources/multimc/64x64/star.png | Bin 0 -> 3851 bytes .../resources/multimc/64x64/status-bad.png | Bin 0 -> 1888 bytes .../resources/multimc/64x64/status-good.png | Bin 0 -> 2258 bytes .../resources/multimc/64x64/status-running.png | Bin 0 -> 2408 bytes .../resources/multimc/64x64/status-yellow.png | Bin 0 -> 1891 bytes .../resources/multimc/64x64/viewfolder.png | Bin 0 -> 1430 bytes .../launcher/resources/multimc/64x64/worlds.png | Bin 0 -> 6756 bytes .../launcher/resources/multimc/8x8/noaccount.png | Bin 0 -> 138 bytes .../launcher/resources/multimc/index.theme | 57 + .../launcher/resources/multimc/multimc.qrc | 340 + .../launcher/resources/multimc/scalable/about.svg | 3928 ++++++++ .../resources/multimc/scalable/about.svg.license | 3 + .../resources/multimc/scalable/accounts.svg | 3 + .../resources/multimc/scalable/adoptium.svg | 180 + .../resources/multimc/scalable/appearance.svg | 2440 +++++ .../multimc/scalable/atlauncher-placeholder.png | Bin 0 -> 10528 bytes .../resources/multimc/scalable/atlauncher.svg | 15 + .../launcher/resources/multimc/scalable/azul.svg | 17 + .../launcher/resources/multimc/scalable/bug.svg | 387 + .../resources/multimc/scalable/centralmods.svg | 346 + .../resources/multimc/scalable/checkupdate.svg | 1566 ++++ .../multimc/scalable/checkupdate.svg.license | 3 + .../resources/multimc/scalable/custom-commands.svg | 4339 +++++++++ .../resources/multimc/scalable/datapacks.svg | 346 + .../launcher/resources/multimc/scalable/delete.svg | 282 + .../resources/multimc/scalable/discord.svg | 11 + .../launcher/resources/multimc/scalable/export.svg | 466 + .../resources/multimc/scalable/instances/bee.svg | 136 + .../multimc/scalable/instances/bee_legacy.svg | 159 + .../resources/multimc/scalable/instances/brick.svg | 67 + .../multimc/scalable/instances/chicken.svg | 130 + .../multimc/scalable/instances/creeper.svg | 68 + .../multimc/scalable/instances/diamond.svg | 62 + .../resources/multimc/scalable/instances/dirt.svg | 52 + .../multimc/scalable/instances/enderman.svg | 96 + .../multimc/scalable/instances/enderpearl.svg | 95 + .../multimc/scalable/instances/fabricmc.svg | 71 + .../resources/multimc/scalable/instances/flame.svg | 49 + .../resources/multimc/scalable/instances/fox.svg | 151 + .../multimc/scalable/instances/fox_legacy.svg | 290 + .../multimc/scalable/instances/ftb_logo.svg | 82 + .../resources/multimc/scalable/instances/gear.svg | 68 + .../resources/multimc/scalable/instances/gold.svg | 63 + .../resources/multimc/scalable/instances/grass.svg | 84 + .../multimc/scalable/instances/herobrine.svg | 111 + .../resources/multimc/scalable/instances/iron.svg | 178 + .../multimc/scalable/instances/magitech.svg | 85 + .../resources/multimc/scalable/instances/meat.svg | 121 + .../multimc/scalable/instances/modrinth.svg | 92 + .../multimc/scalable/instances/neoforged.svg | 3 + .../multimc/scalable/instances/netherstar.svg | 81 + .../multimc/scalable/instances/planks.svg | 93 + .../multimc/scalable/instances/projtlauncher.svg | 131 + .../multimc/scalable/instances/quiltmc.svg | 98 + .../multimc/scalable/instances/skeleton.svg | 134 + .../multimc/scalable/instances/squarecreeper.svg | 81 + .../resources/multimc/scalable/instances/steve.svg | 154 + .../resources/multimc/scalable/instances/stone.svg | 55 + .../resources/multimc/scalable/instances/tnt.svg | 126 + .../launcher/resources/multimc/scalable/java.svg | 773 ++ .../resources/multimc/scalable/language.svg | 109 + .../launcher/resources/multimc/scalable/launch.svg | 96 + .../resources/multimc/scalable/launcher.svg | 181 + .../launcher/resources/multimc/scalable/matrix.svg | 7 + .../launcher/resources/multimc/scalable/mojang.svg | 55 + .../launcher/resources/multimc/scalable/new.svg | 602 ++ .../resources/multimc/scalable/new.svg.license | 3 + .../launcher/resources/multimc/scalable/news.svg | 296 + .../launcher/resources/multimc/scalable/openj9.svg | 17 + .../launcher/resources/multimc/scalable/proxy.svg | 260 + .../resources/multimc/scalable/reddit-alien.svg | 189 + .../launcher/resources/multimc/scalable/rename.svg | 437 + .../multimc/scalable/screenshot-placeholder.svg | 86 + .../resources/multimc/scalable/screenshots.svg | 1231 +++ .../launcher/resources/multimc/scalable/server.svg | 9764 ++++++++++++++++++++ .../resources/multimc/scalable/shortcut.svg | 157 + .../resources/multimc/scalable/status-bad.svg | 142 + .../resources/multimc/scalable/status-good.svg | 201 + .../resources/multimc/scalable/status-running.svg | 187 + .../resources/multimc/scalable/status-yellow.svg | 155 + .../launcher/resources/multimc/scalable/tag.svg | 398 + .../resources/multimc/scalable/technic.svg | 13 + .../resources/multimc/scalable/viewfolder.svg | 122 + .../launcher/resources/pe_blue/index.theme | 11 + .../launcher/resources/pe_blue/pe_blue.qrc | 46 + .../launcher/resources/pe_blue/scalable/about.svg | 16 + .../resources/pe_blue/scalable/accounts.svg | 46 + .../resources/pe_blue/scalable/appearance.svg | 65 + .../launcher/resources/pe_blue/scalable/bug.svg | 47 + .../resources/pe_blue/scalable/centralmods.svg | 43 + .../resources/pe_blue/scalable/checkupdate.svg | 43 + .../launcher/resources/pe_blue/scalable/copy.svg | 41 + .../resources/pe_blue/scalable/coremods.svg | 41 + .../resources/pe_blue/scalable/custom-commands.svg | 336 + .../resources/pe_blue/scalable/datapacks.svg | 345 + .../launcher/resources/pe_blue/scalable/delete.svg | 70 + .../launcher/resources/pe_blue/scalable/export.svg | 40 + .../resources/pe_blue/scalable/externaltools.svg | 41 + .../launcher/resources/pe_blue/scalable/help.svg | 40 + .../pe_blue/scalable/instance-settings.svg | 46 + .../resources/pe_blue/scalable/jarmods.svg | 22 + .../launcher/resources/pe_blue/scalable/java.svg | 47 + .../resources/pe_blue/scalable/language.svg | 46 + .../launcher/resources/pe_blue/scalable/launch.svg | 20 + .../resources/pe_blue/scalable/loadermods.svg | 42 + .../launcher/resources/pe_blue/scalable/log.svg | 41 + .../resources/pe_blue/scalable/minecraft.svg | 44 + .../launcher/resources/pe_blue/scalable/new.svg | 44 + .../launcher/resources/pe_blue/scalable/news.svg | 13 + .../launcher/resources/pe_blue/scalable/notes.svg | 46 + .../resources/pe_blue/scalable/patreon.svg | 41 + .../launcher/resources/pe_blue/scalable/proxy.svg | 45 + .../resources/pe_blue/scalable/refresh.svg | 41 + .../launcher/resources/pe_blue/scalable/rename.svg | 19 + .../resources/pe_blue/scalable/resourcepacks.svg | 13 + .../resources/pe_blue/scalable/screenshots.svg | 44 + .../launcher/resources/pe_blue/scalable/server.svg | 1 + .../resources/pe_blue/scalable/settings.svg | 46 + .../resources/pe_blue/scalable/shaderpacks.svg | 77 + .../resources/pe_blue/scalable/shortcut.svg | 41 + .../resources/pe_blue/scalable/status-bad.svg | 10 + .../resources/pe_blue/scalable/status-good.svg | 15 + .../resources/pe_blue/scalable/status-yellow.svg | 16 + .../launcher/resources/pe_blue/scalable/tag.svg | 39 + .../resources/pe_blue/scalable/viewfolder.svg | 42 + .../launcher/resources/pe_blue/scalable/worlds.svg | 63 + .../launcher/resources/pe_colored/index.theme | 11 + .../launcher/resources/pe_colored/pe_colored.qrc | 46 + .../resources/pe_colored/scalable/about.svg | 19 + .../resources/pe_colored/scalable/accounts.svg | 20 + .../resources/pe_colored/scalable/appearance.svg | 71 + .../launcher/resources/pe_colored/scalable/bug.svg | 17 + .../resources/pe_colored/scalable/centralmods.svg | 18 + .../resources/pe_colored/scalable/checkupdate.svg | 13 + .../resources/pe_colored/scalable/copy.svg | 16 + .../resources/pe_colored/scalable/coremods.svg | 16 + .../pe_colored/scalable/custom-commands.svg | 345 + .../resources/pe_colored/scalable/datapacks.svg | 347 + .../resources/pe_colored/scalable/delete.svg | 70 + .../resources/pe_colored/scalable/export.svg | 44 + .../pe_colored/scalable/externaltools.svg | 13 + .../resources/pe_colored/scalable/help.svg | 46 + .../pe_colored/scalable/instance-settings.svg | 18 + .../resources/pe_colored/scalable/jarmods.svg | 22 + .../resources/pe_colored/scalable/java.svg | 49 + .../resources/pe_colored/scalable/language.svg | 44 + .../resources/pe_colored/scalable/launch.svg | 23 + .../resources/pe_colored/scalable/loadermods.svg | 15 + .../launcher/resources/pe_colored/scalable/log.svg | 16 + .../resources/pe_colored/scalable/minecraft.svg | 14 + .../launcher/resources/pe_colored/scalable/new.svg | 16 + .../resources/pe_colored/scalable/news.svg | 13 + .../resources/pe_colored/scalable/notes.svg | 21 + .../resources/pe_colored/scalable/patreon.svg | 12 + .../resources/pe_colored/scalable/proxy.svg | 15 + .../resources/pe_colored/scalable/refresh.svg | 11 + .../resources/pe_colored/scalable/rename.svg | 22 + .../pe_colored/scalable/resourcepacks.svg | 15 + .../resources/pe_colored/scalable/screenshots.svg | 16 + .../resources/pe_colored/scalable/server.svg | 1 + .../resources/pe_colored/scalable/settings.svg | 18 + .../resources/pe_colored/scalable/shaderpacks.svg | 83 + .../resources/pe_colored/scalable/shortcut.svg | 13 + .../resources/pe_colored/scalable/status-bad.svg | 10 + .../resources/pe_colored/scalable/status-good.svg | 15 + .../pe_colored/scalable/status-yellow.svg | 16 + .../launcher/resources/pe_colored/scalable/tag.svg | 42 + .../resources/pe_colored/scalable/viewfolder.svg | 17 + .../resources/pe_colored/scalable/worlds.svg | 50 + .../launcher/resources/pe_dark/index.theme | 11 + .../launcher/resources/pe_dark/pe_dark.qrc | 46 + .../launcher/resources/pe_dark/scalable/about.svg | 15 + .../resources/pe_dark/scalable/accounts.svg | 46 + .../resources/pe_dark/scalable/appearance.svg | 65 + .../launcher/resources/pe_dark/scalable/bug.svg | 47 + .../resources/pe_dark/scalable/centralmods.svg | 40 + .../resources/pe_dark/scalable/checkupdate.svg | 12 + .../launcher/resources/pe_dark/scalable/copy.svg | 40 + .../resources/pe_dark/scalable/coremods.svg | 41 + .../resources/pe_dark/scalable/custom-commands.svg | 335 + .../resources/pe_dark/scalable/datapacks.svg | 345 + .../launcher/resources/pe_dark/scalable/delete.svg | 70 + .../launcher/resources/pe_dark/scalable/export.svg | 36 + .../resources/pe_dark/scalable/externaltools.svg | 41 + .../launcher/resources/pe_dark/scalable/help.svg | 34 + .../pe_dark/scalable/instance-settings.svg | 43 + .../resources/pe_dark/scalable/jarmods.svg | 41 + .../launcher/resources/pe_dark/scalable/java.svg | 48 + .../resources/pe_dark/scalable/language.svg | 45 + .../launcher/resources/pe_dark/scalable/launch.svg | 17 + .../resources/pe_dark/scalable/loadermods.svg | 42 + .../launcher/resources/pe_dark/scalable/log.svg | 41 + .../resources/pe_dark/scalable/minecraft.svg | 44 + .../launcher/resources/pe_dark/scalable/new.svg | 41 + .../launcher/resources/pe_dark/scalable/news.svg | 13 + .../launcher/resources/pe_dark/scalable/notes.svg | 46 + .../resources/pe_dark/scalable/patreon.svg | 41 + .../launcher/resources/pe_dark/scalable/proxy.svg | 43 + .../resources/pe_dark/scalable/refresh.svg | 11 + .../launcher/resources/pe_dark/scalable/rename.svg | 19 + .../resources/pe_dark/scalable/resourcepacks.svg | 13 + .../resources/pe_dark/scalable/screenshots.svg | 44 + .../launcher/resources/pe_dark/scalable/server.svg | 1 + .../resources/pe_dark/scalable/settings.svg | 43 + .../resources/pe_dark/scalable/shaderpacks.svg | 81 + .../resources/pe_dark/scalable/shortcut.svg | 41 + .../resources/pe_dark/scalable/status-bad.svg | 10 + .../resources/pe_dark/scalable/status-good.svg | 12 + .../resources/pe_dark/scalable/status-yellow.svg | 16 + .../launcher/resources/pe_dark/scalable/tag.svg | 30 + .../resources/pe_dark/scalable/viewfolder.svg | 39 + .../launcher/resources/pe_dark/scalable/worlds.svg | 63 + .../launcher/resources/pe_light/index.theme | 11 + .../launcher/resources/pe_light/pe_light.qrc | 46 + .../launcher/resources/pe_light/scalable/about.svg | 15 + .../resources/pe_light/scalable/accounts.svg | 46 + .../resources/pe_light/scalable/appearance.svg | 66 + .../launcher/resources/pe_light/scalable/bug.svg | 47 + .../resources/pe_light/scalable/centralmods.svg | 41 + .../resources/pe_light/scalable/checkupdate.svg | 13 + .../launcher/resources/pe_light/scalable/copy.svg | 40 + .../resources/pe_light/scalable/coremods.svg | 41 + .../pe_light/scalable/custom-commands.svg | 336 + .../resources/pe_light/scalable/datapacks.svg | 345 + .../resources/pe_light/scalable/delete.svg | 70 + .../resources/pe_light/scalable/export.svg | 37 + .../resources/pe_light/scalable/externaltools.svg | 41 + .../launcher/resources/pe_light/scalable/help.svg | 36 + .../pe_light/scalable/instance-settings.svg | 43 + .../resources/pe_light/scalable/jarmods.svg | 41 + .../launcher/resources/pe_light/scalable/java.svg | 49 + .../resources/pe_light/scalable/language.svg | 80 + .../resources/pe_light/scalable/launch.svg | 17 + .../resources/pe_light/scalable/loadermods.svg | 42 + .../launcher/resources/pe_light/scalable/log.svg | 41 + .../resources/pe_light/scalable/minecraft.svg | 44 + .../launcher/resources/pe_light/scalable/new.svg | 41 + .../launcher/resources/pe_light/scalable/news.svg | 12 + .../launcher/resources/pe_light/scalable/notes.svg | 46 + .../resources/pe_light/scalable/patreon.svg | 40 + .../launcher/resources/pe_light/scalable/proxy.svg | 45 + .../resources/pe_light/scalable/refresh.svg | 11 + .../resources/pe_light/scalable/rename.svg | 19 + .../resources/pe_light/scalable/resourcepacks.svg | 13 + .../resources/pe_light/scalable/screenshots.svg | 44 + .../resources/pe_light/scalable/server.svg | 1 + .../resources/pe_light/scalable/settings.svg | 43 + .../resources/pe_light/scalable/shaderpacks.svg | 81 + .../resources/pe_light/scalable/shortcut.svg | 41 + .../resources/pe_light/scalable/status-bad.svg | 10 + .../resources/pe_light/scalable/status-good.svg | 12 + .../resources/pe_light/scalable/status-yellow.svg | 16 + .../launcher/resources/pe_light/scalable/tag.svg | 23 + .../resources/pe_light/scalable/viewfolder.svg | 40 + .../resources/pe_light/scalable/worlds.svg | 64 + .../launcher/resources/shaders/fshader.glsl | 20 + .../launcher/resources/shaders/shaders.qrc | 7 + .../resources/shaders/vshader_skin_background.glsl | 11 + .../resources/shaders/vshader_skin_model.glsl | 37 + .../launcher/resources/sources/burfcat_hat.png | Bin 0 -> 9969 bytes .../launcher/resources/sources/cattiversary.xcf | Bin 0 -> 121675 bytes .../launcher/resources/sources/clucker.svg | 404 + .../launcher/resources/sources/creeper.svg | 775 ++ .../launcher/resources/sources/enderpearl.svg | 271 + .../launcher/resources/sources/flame.svg | 51 + .../launcher/resources/sources/ftb-glow.svg | 606 ++ .../launcher/resources/sources/ftb-logo.svg | 257 + .../launcher/resources/sources/gear.svg | 434 + .../launcher/resources/sources/herobrine.svg | 583 ++ .../launcher/resources/sources/magitech.svg | 886 ++ .../launcher/resources/sources/meat.svg | 371 + .../launcher/resources/sources/netherstar.svg | 342 + .../launcher/resources/sources/pskeleton.svg | 581 ++ .../launcher/resources/sources/skeleton.svg | 610 ++ .../launcher/resources/sources/squarecreeper.svg | 828 ++ .../launcher/resources/sources/steve.svg | 534 ++ .../launcher/screenshots/ImgurAlbumCreation.cpp | 140 + .../launcher/screenshots/ImgurAlbumCreation.h | 102 + .../launcher/screenshots/ImgurUpload.cpp | 155 + .../launcher/screenshots/ImgurUpload.h | 98 + .../launcher/screenshots/Screenshot.h | 40 + .../projt-launcher/launcher/settings/INIFile.cpp | 303 + .../projt-launcher/launcher/settings/INIFile.h | 82 + .../launcher/settings/INISettingsObject.cpp | 152 + .../launcher/settings/INISettingsObject.h | 88 + .../launcher/settings/OverrideSetting.cpp | 76 + .../launcher/settings/OverrideSetting.h | 68 + .../launcher/settings/PassthroughSetting.cpp | 91 + .../launcher/settings/PassthroughSetting.h | 67 + .../projt-launcher/launcher/settings/Setting.cpp | 73 + .../projt-launcher/launcher/settings/Setting.h | 138 + .../launcher/settings/SettingsObject.cpp | 163 + .../launcher/settings/SettingsObject.h | 247 + .../launcher/tasks/ConcurrentTask.cpp | 333 + .../projt-launcher/launcher/tasks/ConcurrentTask.h | 145 + .../launcher/tasks/MultipleOptionsTask.cpp | 89 + .../launcher/tasks/MultipleOptionsTask.h | 76 + .../launcher/tasks/SequentialTask.cpp | 80 + .../projt-launcher/launcher/tasks/SequentialTask.h | 83 + archived/projt-launcher/launcher/tasks/Task.cpp | 239 + archived/projt-launcher/launcher/tasks/Task.h | 288 + .../launcher/tools/BaseExternalTool.cpp | 56 + .../launcher/tools/BaseExternalTool.h | 77 + .../projt-launcher/launcher/tools/BaseProfiler.cpp | 55 + .../projt-launcher/launcher/tools/BaseProfiler.h | 59 + .../launcher/tools/GenericProfiler.cpp | 74 + .../launcher/tools/GenericProfiler.h | 66 + .../projt-launcher/launcher/tools/JProfiler.cpp | 130 + archived/projt-launcher/launcher/tools/JProfiler.h | 36 + .../projt-launcher/launcher/tools/JVisualVM.cpp | 119 + archived/projt-launcher/launcher/tools/JVisualVM.h | 36 + .../projt-launcher/launcher/tools/MCEditTool.cpp | 98 + .../projt-launcher/launcher/tools/MCEditTool.h | 37 + .../launcher/translations/POTranslator.cpp | 364 + .../launcher/translations/POTranslator.h | 38 + .../launcher/translations/TranslationsModel.cpp | 846 ++ .../launcher/translations/TranslationsModel.h | 88 + archived/projt-launcher/launcher/ui/GuiUtil.cpp | 312 + archived/projt-launcher/launcher/ui/GuiUtil.h | 39 + .../projt-launcher/launcher/ui/InstanceWindow.cpp | 267 + .../projt-launcher/launcher/ui/InstanceWindow.h | 118 + archived/projt-launcher/launcher/ui/LaunchMenu.cpp | 98 + archived/projt-launcher/launcher/ui/LaunchMenu.h | 30 + archived/projt-launcher/launcher/ui/MainWindow.cpp | 2134 +++++ archived/projt-launcher/launcher/ui/MainWindow.h | 296 + archived/projt-launcher/launcher/ui/MainWindow.ui | 815 ++ .../projt-launcher/launcher/ui/ViewLogWindow.cpp | 46 + .../projt-launcher/launcher/ui/ViewLogWindow.h | 44 + .../launcher/ui/dialogs/AboutDialog.cpp | 197 + .../launcher/ui/dialogs/AboutDialog.h | 57 + .../launcher/ui/dialogs/AboutDialog.ui | 390 + .../launcher/ui/dialogs/BackupDialog.cpp | 330 + .../launcher/ui/dialogs/BackupDialog.h | 63 + .../launcher/ui/dialogs/BackupDialog.ui | 228 + .../launcher/ui/dialogs/BlockedModsDialog.cpp | 530 ++ .../launcher/ui/dialogs/BlockedModsDialog.h | 129 + .../launcher/ui/dialogs/BlockedModsDialog.ui | 205 + .../launcher/ui/dialogs/ChooseProviderDialog.cpp | 118 + .../launcher/ui/dialogs/ChooseProviderDialog.h | 82 + .../launcher/ui/dialogs/ChooseProviderDialog.ui | 89 + .../launcher/ui/dialogs/CopyInstanceDialog.cpp | 353 + .../launcher/ui/dialogs/CopyInstanceDialog.h | 102 + .../launcher/ui/dialogs/CopyInstanceDialog.ui | 447 + .../launcher/ui/dialogs/CreateShortcutDialog.cpp | 264 + .../launcher/ui/dialogs/CreateShortcutDialog.h | 83 + .../launcher/ui/dialogs/CreateShortcutDialog.ui | 264 + .../launcher/ui/dialogs/CustomMessageBox.cpp | 63 + .../launcher/ui/dialogs/CustomMessageBox.h | 51 + .../launcher/ui/dialogs/ExportInstanceDialog.cpp | 240 + .../launcher/ui/dialogs/ExportInstanceDialog.h | 99 + .../launcher/ui/dialogs/ExportInstanceDialog.ui | 83 + .../launcher/ui/dialogs/ExportPackDialog.cpp | 284 + .../launcher/ui/dialogs/ExportPackDialog.h | 79 + .../launcher/ui/dialogs/ExportPackDialog.ui | 267 + .../launcher/ui/dialogs/ExportToModListDialog.cpp | 264 + .../launcher/ui/dialogs/ExportToModListDialog.h | 84 + .../launcher/ui/dialogs/ExportToModListDialog.ui | 276 + .../launcher/ui/dialogs/IconPickerDialog.cpp | 217 + .../launcher/ui/dialogs/IconPickerDialog.h | 76 + .../launcher/ui/dialogs/IconPickerDialog.ui | 67 + .../launcher/ui/dialogs/ImportResourceDialog.cpp | 101 + .../launcher/ui/dialogs/ImportResourceDialog.h | 52 + .../launcher/ui/dialogs/ImportResourceDialog.ui | 81 + .../launcher/ui/dialogs/InstallLoaderDialog.cpp | 233 + .../launcher/ui/dialogs/InstallLoaderDialog.h | 72 + .../launcher/ui/dialogs/LauncherHubDialog.cpp | 79 + .../launcher/ui/dialogs/LauncherHubDialog.h | 50 + .../launcher/ui/dialogs/MSALoginDialog.cpp | 294 + .../launcher/ui/dialogs/MSALoginDialog.h | 78 + .../launcher/ui/dialogs/MSALoginDialog.ui | 429 + .../launcher/ui/dialogs/NewComponentDialog.cpp | 154 + .../launcher/ui/dialogs/NewComponentDialog.h | 72 + .../launcher/ui/dialogs/NewComponentDialog.ui | 101 + .../launcher/ui/dialogs/NewInstanceDialog.cpp | 381 + .../launcher/ui/dialogs/NewInstanceDialog.h | 131 + .../launcher/ui/dialogs/NewInstanceDialog.ui | 91 + .../launcher/ui/dialogs/NewsDialog.cpp | 116 + .../launcher/ui/dialogs/NewsDialog.h | 57 + .../launcher/ui/dialogs/NewsDialog.ui | 130 + .../launcher/ui/dialogs/OfflineLoginDialog.cpp | 133 + .../launcher/ui/dialogs/OfflineLoginDialog.h | 62 + .../launcher/ui/dialogs/OfflineLoginDialog.ui | 80 + .../launcher/ui/dialogs/ProfileSelectDialog.cpp | 143 + .../launcher/ui/dialogs/ProfileSelectDialog.h | 111 + .../launcher/ui/dialogs/ProfileSelectDialog.ui | 62 + .../launcher/ui/dialogs/ProfileSetupDialog.cpp | 345 + .../launcher/ui/dialogs/ProfileSetupDialog.h | 109 + .../launcher/ui/dialogs/ProfileSetupDialog.ui | 77 + .../launcher/ui/dialogs/ProgressDialog.cpp | 361 + .../launcher/ui/dialogs/ProgressDialog.h | 134 + .../launcher/ui/dialogs/ProgressDialog.ui | 144 + .../launcher/ui/dialogs/ResourceDownloadDialog.cpp | 572 ++ .../launcher/ui/dialogs/ResourceDownloadDialog.h | 278 + .../launcher/ui/dialogs/ResourceUpdateDialog.cpp | 659 ++ .../launcher/ui/dialogs/ResourceUpdateDialog.h | 98 + .../launcher/ui/dialogs/ReviewMessageBox.cpp | 188 + .../launcher/ui/dialogs/ReviewMessageBox.h | 66 + .../launcher/ui/dialogs/ReviewMessageBox.ui | 74 + .../launcher/ui/dialogs/ScrollMessageBox.cpp | 41 + .../launcher/ui/dialogs/ScrollMessageBox.h | 43 + .../launcher/ui/dialogs/ScrollMessageBox.ui | 84 + .../launcher/ui/dialogs/UpdateAvailableDialog.cpp | 115 + .../launcher/ui/dialogs/UpdateAvailableDialog.h | 83 + .../launcher/ui/dialogs/UpdateAvailableDialog.ui | 155 + .../launcher/ui/dialogs/VersionSelectDialog.cpp | 190 + .../launcher/ui/dialogs/VersionSelectDialog.h | 107 + .../launcher/ui/dialogs/skins/SkinManageDialog.cpp | 715 ++ .../launcher/ui/dialogs/skins/SkinManageDialog.h | 100 + .../launcher/ui/dialogs/skins/SkinManageDialog.ui | 223 + .../launcher/ui/dialogs/skins/draw/BoxGeometry.cpp | 342 + .../launcher/ui/dialogs/skins/draw/BoxGeometry.h | 85 + .../launcher/ui/dialogs/skins/draw/Scene.cpp | 238 + .../launcher/ui/dialogs/skins/draw/Scene.h | 75 + .../ui/dialogs/skins/draw/SkinOpenGLWindow.cpp | 374 + .../ui/dialogs/skins/draw/SkinOpenGLWindow.h | 109 + .../ui/instanceview/AccessibleInstanceView.cpp | 888 ++ .../ui/instanceview/AccessibleInstanceView.h | 27 + .../ui/instanceview/AccessibleInstanceView_p.h | 152 + .../launcher/ui/instanceview/InstanceDelegate.cpp | 427 + .../launcher/ui/instanceview/InstanceDelegate.h | 67 + .../ui/instanceview/InstanceProxyModel.cpp | 135 + .../launcher/ui/instanceview/InstanceProxyModel.h | 61 + .../launcher/ui/instanceview/InstanceView.cpp | 1197 +++ .../launcher/ui/instanceview/InstanceView.h | 203 + .../launcher/ui/instanceview/VisualGroup.cpp | 311 + .../launcher/ui/instanceview/VisualGroup.h | 150 + .../launcher/ui/java/InstallJavaDialog.cpp | 465 + .../launcher/ui/java/InstallJavaDialog.h | 54 + .../launcher/ui/java/VersionList.cpp | 139 + .../projt-launcher/launcher/ui/java/VersionList.h | 56 + .../launcher/ui/pagedialog/PageDialog.cpp | 105 + .../launcher/ui/pagedialog/PageDialog.h | 62 + .../projt-launcher/launcher/ui/pages/BasePage.h | 121 + .../launcher/ui/pages/BasePageContainer.h | 37 + .../launcher/ui/pages/BasePageProvider.h | 92 + .../launcher/ui/pages/global/APIPage.cpp | 242 + .../launcher/ui/pages/global/APIPage.h | 110 + .../launcher/ui/pages/global/APIPage.ui | 428 + .../launcher/ui/pages/global/AccountListPage.cpp | 291 + .../launcher/ui/pages/global/AccountListPage.h | 129 + .../launcher/ui/pages/global/AccountListPage.ui | 123 + .../launcher/ui/pages/global/AppearancePage.h | 108 + .../launcher/ui/pages/global/ExternalToolsPage.cpp | 280 + .../launcher/ui/pages/global/ExternalToolsPage.h | 118 + .../launcher/ui/pages/global/ExternalToolsPage.ui | 301 + .../launcher/ui/pages/global/JavaPage.cpp | 125 + .../launcher/ui/pages/global/JavaPage.h | 73 + .../launcher/ui/pages/global/JavaPage.ui | 153 + .../launcher/ui/pages/global/LanguagePage.cpp | 101 + .../launcher/ui/pages/global/LanguagePage.h | 102 + .../launcher/ui/pages/global/LauncherPage.cpp | 364 + .../launcher/ui/pages/global/LauncherPage.h | 118 + .../launcher/ui/pages/global/LauncherPage.ui | 753 ++ .../launcher/ui/pages/global/MinecraftPage.h | 101 + .../launcher/ui/pages/global/ProxyPage.cpp | 152 + .../launcher/ui/pages/global/ProxyPage.h | 111 + .../launcher/ui/pages/global/ProxyPage.ui | 230 + .../launcher/ui/pages/instance/BackupPage.cpp | 372 + .../launcher/ui/pages/instance/BackupPage.h | 85 + .../launcher/ui/pages/instance/DataPackPage.cpp | 480 + .../launcher/ui/pages/instance/DataPackPage.h | 126 + .../ui/pages/instance/ExternalResourcesPage.cpp | 407 + .../ui/pages/instance/ExternalResourcesPage.h | 102 + .../ui/pages/instance/ExternalResourcesPage.ui | 245 + .../ui/pages/instance/InstanceSettingsPage.h | 102 + .../launcher/ui/pages/instance/LogPage.cpp | 419 + .../launcher/ui/pages/instance/LogPage.h | 152 + .../launcher/ui/pages/instance/LogPage.ui | 183 + .../launcher/ui/pages/instance/ManagedPackPage.cpp | 625 ++ .../launcher/ui/pages/instance/ManagedPackPage.h | 210 + .../launcher/ui/pages/instance/ManagedPackPage.ui | 216 + .../launcher/ui/pages/instance/McClient.cpp | 214 + .../launcher/ui/pages/instance/McClient.h | 74 + .../launcher/ui/pages/instance/McResolver.cpp | 114 + .../launcher/ui/pages/instance/McResolver.h | 49 + .../launcher/ui/pages/instance/ModFolderPage.cpp | 498 + .../launcher/ui/pages/instance/ModFolderPage.h | 171 + .../launcher/ui/pages/instance/NotesPage.cpp | 84 + .../launcher/ui/pages/instance/NotesPage.h | 104 + .../launcher/ui/pages/instance/NotesPage.ui | 43 + .../launcher/ui/pages/instance/OtherLogsPage.cpp | 675 ++ .../launcher/ui/pages/instance/OtherLogsPage.h | 152 + .../launcher/ui/pages/instance/OtherLogsPage.ui | 230 + .../ui/pages/instance/ResourcePackPage.cpp | 354 + .../launcher/ui/pages/instance/ResourcePackPage.h | 116 + .../launcher/ui/pages/instance/ScreenshotsPage.cpp | 741 ++ .../launcher/ui/pages/instance/ScreenshotsPage.h | 143 + .../launcher/ui/pages/instance/ScreenshotsPage.ui | 114 + .../launcher/ui/pages/instance/ServerPingTask.cpp | 69 + .../launcher/ui/pages/instance/ServerPingTask.h | 43 + .../launcher/ui/pages/instance/ServersPage.cpp | 911 ++ .../launcher/ui/pages/instance/ServersPage.h | 144 + .../launcher/ui/pages/instance/ServersPage.ui | 204 + .../launcher/ui/pages/instance/ShaderPackPage.cpp | 343 + .../launcher/ui/pages/instance/ShaderPackPage.h | 110 + .../launcher/ui/pages/instance/TexturePackPage.cpp | 351 + .../launcher/ui/pages/instance/TexturePackPage.h | 114 + .../launcher/ui/pages/instance/VersionPage.cpp | 717 ++ .../launcher/ui/pages/instance/VersionPage.h | 157 + .../launcher/ui/pages/instance/VersionPage.ui | 260 + .../launcher/ui/pages/instance/WorldListPage.cpp | 563 ++ .../launcher/ui/pages/instance/WorldListPage.h | 144 + .../launcher/ui/pages/instance/WorldListPage.ui | 170 + .../launcher/ui/pages/modplatform/CustomPage.cpp | 274 + .../launcher/ui/pages/modplatform/CustomPage.h | 128 + .../launcher/ui/pages/modplatform/CustomPage.ui | 293 + .../ui/pages/modplatform/DataPackModel.cpp | 84 + .../launcher/ui/pages/modplatform/DataPackModel.h | 77 + .../launcher/ui/pages/modplatform/DataPackPage.cpp | 74 + .../launcher/ui/pages/modplatform/DataPackPage.h | 88 + .../launcher/ui/pages/modplatform/ImportPage.cpp | 304 + .../launcher/ui/pages/modplatform/ImportPage.h | 113 + .../launcher/ui/pages/modplatform/ImportPage.ui | 106 + .../launcher/ui/pages/modplatform/ModModel.cpp | 179 + .../launcher/ui/pages/modplatform/ModModel.h | 94 + .../launcher/ui/pages/modplatform/ModPage.cpp | 155 + .../launcher/ui/pages/modplatform/ModPage.h | 115 + .../ui/pages/modplatform/ModpackProviderBasePage.h | 54 + .../ui/pages/modplatform/OptionalModDialog.cpp | 105 + .../ui/pages/modplatform/OptionalModDialog.h | 65 + .../ui/pages/modplatform/OptionalModDialog.ui | 116 + .../ui/pages/modplatform/ResourceModel.cpp | 608 ++ .../launcher/ui/pages/modplatform/ResourceModel.h | 224 + .../ui/pages/modplatform/ResourcePackModel.cpp | 79 + .../ui/pages/modplatform/ResourcePackModel.h | 76 + .../ui/pages/modplatform/ResourcePackPage.cpp | 74 + .../ui/pages/modplatform/ResourcePackPage.h | 92 + .../launcher/ui/pages/modplatform/ResourcePage.cpp | 679 ++ .../launcher/ui/pages/modplatform/ResourcePage.h | 170 + .../launcher/ui/pages/modplatform/ResourcePage.ui | 104 + .../ui/pages/modplatform/ShaderPackModel.cpp | 79 + .../ui/pages/modplatform/ShaderPackModel.h | 76 + .../ui/pages/modplatform/ShaderPackPage.cpp | 87 + .../launcher/ui/pages/modplatform/ShaderPackPage.h | 96 + .../ui/pages/modplatform/TexturePackModel.cpp | 120 + .../ui/pages/modplatform/TexturePackModel.h | 57 + .../ui/pages/modplatform/TexturePackPage.h | 81 + .../modplatform/atlauncher/AtlFilterModel.cpp | 132 + .../pages/modplatform/atlauncher/AtlFilterModel.h | 72 + .../pages/modplatform/atlauncher/AtlListModel.cpp | 266 + .../ui/pages/modplatform/atlauncher/AtlListModel.h | 90 + .../atlauncher/AtlOptionalModDialog.cpp | 454 + .../modplatform/atlauncher/AtlOptionalModDialog.h | 149 + .../modplatform/atlauncher/AtlOptionalModDialog.ui | 69 + .../ui/pages/modplatform/atlauncher/AtlPage.cpp | 215 + .../ui/pages/modplatform/atlauncher/AtlPage.h | 131 + .../ui/pages/modplatform/atlauncher/AtlPage.ui | 103 + .../atlauncher/AtlUserInteractionSupportImpl.cpp | 129 + .../atlauncher/AtlUserInteractionSupportImpl.h | 82 + .../ui/pages/modplatform/flame/FlameModel.cpp | 342 + .../ui/pages/modplatform/flame/FlameModel.h | 110 + .../ui/pages/modplatform/flame/FlamePage.cpp | 409 + .../ui/pages/modplatform/flame/FlamePage.h | 144 + .../ui/pages/modplatform/flame/FlamePage.ui | 126 + .../modplatform/flame/FlameResourceModels.cpp | 77 + .../pages/modplatform/flame/FlameResourceModels.h | 61 + .../pages/modplatform/flame/FlameResourcePages.cpp | 332 + .../pages/modplatform/flame/FlameResourcePages.h | 330 + .../pages/modplatform/import_ftb/ImportFTBPage.cpp | 234 + .../pages/modplatform/import_ftb/ImportFTBPage.h | 116 + .../pages/modplatform/import_ftb/ImportFTBPage.ui | 104 + .../ui/pages/modplatform/import_ftb/ListModel.cpp | 264 + .../ui/pages/modplatform/import_ftb/ListModel.h | 108 + .../ui/pages/modplatform/legacy_ftb/ListModel.cpp | 362 + .../ui/pages/modplatform/legacy_ftb/ListModel.h | 100 + .../ui/pages/modplatform/legacy_ftb/Page.cpp | 477 + .../ui/pages/modplatform/legacy_ftb/Page.h | 169 + .../ui/pages/modplatform/legacy_ftb/Page.ui | 170 + .../pages/modplatform/modrinth/ModrinthModel.cpp | 435 + .../ui/pages/modplatform/modrinth/ModrinthModel.h | 185 + .../ui/pages/modplatform/modrinth/ModrinthPage.cpp | 458 + .../ui/pages/modplatform/modrinth/ModrinthPage.h | 161 + .../ui/pages/modplatform/modrinth/ModrinthPage.ui | 108 + .../modplatform/modrinth/ModrinthResourcePages.cpp | 251 + .../modplatform/modrinth/ModrinthResourcePages.h | 317 + .../ui/pages/modplatform/technic/TechnicData.h | 94 + .../ui/pages/modplatform/technic/TechnicModel.cpp | 387 + .../ui/pages/modplatform/technic/TechnicModel.h | 130 + .../ui/pages/modplatform/technic/TechnicPage.cpp | 443 + .../ui/pages/modplatform/technic/TechnicPage.h | 143 + .../ui/pages/modplatform/technic/TechnicPage.ui | 85 + .../launcher/ui/setupwizard/AutoJavaWizardPage.cpp | 55 + .../launcher/ui/setupwizard/AutoJavaWizardPage.h | 44 + .../launcher/ui/setupwizard/AutoJavaWizardPage.ui | 93 + .../launcher/ui/setupwizard/BaseWizardPage.h | 50 + .../launcher/ui/setupwizard/JavaWizardPage.cpp | 115 + .../launcher/ui/setupwizard/JavaWizardPage.h | 46 + .../launcher/ui/setupwizard/LanguageWizardPage.cpp | 67 + .../launcher/ui/setupwizard/LanguageWizardPage.h | 46 + .../launcher/ui/setupwizard/LoginWizardPage.cpp | 69 + .../launcher/ui/setupwizard/LoginWizardPage.h | 46 + .../launcher/ui/setupwizard/LoginWizardPage.ui | 74 + .../launcher/ui/setupwizard/PasteWizardPage.cpp | 60 + .../launcher/ui/setupwizard/PasteWizardPage.h | 48 + .../launcher/ui/setupwizard/PasteWizardPage.ui | 80 + .../launcher/ui/setupwizard/SearchWizardPage.cpp | 61 + .../launcher/ui/setupwizard/SearchWizardPage.h | 27 + .../launcher/ui/setupwizard/SetupWizard.cpp | 113 + .../launcher/ui/setupwizard/SetupWizard.h | 66 + .../launcher/ui/setupwizard/ThemeWizardPage.h | 77 + .../launcher/ui/tasks/LogUploadTask.cpp | 161 + .../launcher/ui/tasks/LogUploadTask.h | 55 + .../launcher/ui/themes/BrightTheme.cpp | 114 + .../launcher/ui/themes/BrightTheme.h | 77 + .../projt-launcher/launcher/ui/themes/CatPack.cpp | 173 + .../projt-launcher/launcher/ui/themes/CatPack.h | 136 + .../launcher/ui/themes/CatPainter.cpp | 97 + .../projt-launcher/launcher/ui/themes/CatPainter.h | 64 + .../launcher/ui/themes/CustomTheme.cpp | 345 + .../launcher/ui/themes/CustomTheme.h | 105 + .../launcher/ui/themes/DarkTheme.cpp | 117 + .../projt-launcher/launcher/ui/themes/DarkTheme.h | 77 + .../launcher/ui/themes/FusionTheme.cpp | 26 + .../launcher/ui/themes/FusionTheme.h | 32 + .../launcher/ui/themes/HintOverrideProxyStyle.cpp | 65 + .../launcher/ui/themes/HintOverrideProxyStyle.h | 59 + .../launcher/ui/themes/IconTheme.cpp | 60 + .../projt-launcher/launcher/ui/themes/IconTheme.h | 72 + .../launcher/ui/themes/SystemTheme.cpp | 186 + .../launcher/ui/themes/SystemTheme.h | 85 + .../projt-launcher/launcher/ui/themes/Theme.cpp | 127 + archived/projt-launcher/launcher/ui/themes/Theme.h | 100 + .../launcher/ui/themes/ThemeManager.cpp | 425 + .../launcher/ui/themes/ThemeManager.h | 120 + .../launcher/ui/themes/ThemeManager.mm | 98 + .../launcher/ui/widgets/AppearanceWidget.cpp | 326 + .../launcher/ui/widgets/AppearanceWidget.h | 87 + .../launcher/ui/widgets/AppearanceWidget.ui | 632 ++ .../launcher/ui/widgets/CefHubView.cpp | 1334 +++ .../launcher/ui/widgets/CefHubView.h | 111 + .../launcher/ui/widgets/CheckComboBox.cpp | 250 + .../launcher/ui/widgets/CheckComboBox.h | 89 + .../projt-launcher/launcher/ui/widgets/Common.cpp | 54 + .../projt-launcher/launcher/ui/widgets/Common.h | 29 + .../launcher/ui/widgets/CustomCommands.cpp | 115 + .../launcher/ui/widgets/CustomCommands.h | 91 + .../launcher/ui/widgets/CustomCommands.ui | 158 + .../launcher/ui/widgets/EnvironmentVariables.cpp | 148 + .../launcher/ui/widgets/EnvironmentVariables.h | 69 + .../launcher/ui/widgets/EnvironmentVariables.ui | 122 + .../launcher/ui/widgets/FallbackHubView.cpp | 100 + .../launcher/ui/widgets/FallbackHubView.h | 52 + .../launcher/ui/widgets/FastFileIconProvider.cpp | 71 + .../launcher/ui/widgets/FastFileIconProvider.h | 49 + .../launcher/ui/widgets/FileIgnoreProxy.cpp | 362 + .../launcher/ui/widgets/FileIgnoreProxy.h | 123 + .../launcher/ui/widgets/HubSearchProvider.cpp | 97 + .../launcher/ui/widgets/HubSearchProvider.h | 21 + .../launcher/ui/widgets/HubViewBase.h | 55 + .../launcher/ui/widgets/IconLabel.cpp | 62 + .../projt-launcher/launcher/ui/widgets/IconLabel.h | 46 + .../launcher/ui/widgets/InfoFrame.cpp | 524 ++ .../projt-launcher/launcher/ui/widgets/InfoFrame.h | 111 + .../launcher/ui/widgets/InfoFrame.ui | 158 + .../launcher/ui/widgets/JavaSettingsWidget.cpp | 356 + .../launcher/ui/widgets/JavaSettingsWidget.h | 56 + .../launcher/ui/widgets/JavaSettingsWidget.ui | 392 + .../launcher/ui/widgets/JavaWizardWidget.cpp | 645 ++ .../launcher/ui/widgets/JavaWizardWidget.h | 137 + .../launcher/ui/widgets/LabeledToolButton.cpp | 155 + .../launcher/ui/widgets/LabeledToolButton.h | 63 + .../ui/widgets/LanguageSelectionWidget.cpp | 111 + .../launcher/ui/widgets/LanguageSelectionWidget.h | 67 + .../launcher/ui/widgets/LauncherHubWidget.cpp | 1297 +++ .../launcher/ui/widgets/LauncherHubWidget.h | 116 + .../projt-launcher/launcher/ui/widgets/LogView.cpp | 229 + .../projt-launcher/launcher/ui/widgets/LogView.h | 58 + .../ui/widgets/MinecraftSettingsWidget.cpp | 676 ++ .../launcher/ui/widgets/MinecraftSettingsWidget.h | 94 + .../launcher/ui/widgets/MinecraftSettingsWidget.ui | 859 ++ .../launcher/ui/widgets/ModFilterWidget.cpp | 479 + .../launcher/ui/widgets/ModFilterWidget.h | 168 + .../launcher/ui/widgets/ModFilterWidget.ui | 333 + .../launcher/ui/widgets/ModListView.cpp | 96 + .../launcher/ui/widgets/ModListView.h | 49 + .../launcher/ui/widgets/PageContainer.cpp | 334 + .../launcher/ui/widgets/PageContainer.h | 150 + .../launcher/ui/widgets/PageContainer_p.h | 145 + .../launcher/ui/widgets/ProgressWidget.cpp | 143 + .../launcher/ui/widgets/ProgressWidget.h | 87 + .../launcher/ui/widgets/ProjectDescriptionPage.cpp | 45 + .../launcher/ui/widgets/ProjectDescriptionPage.h | 53 + .../launcher/ui/widgets/ProjectItem.cpp | 238 + .../launcher/ui/widgets/ProjectItem.h | 57 + .../launcher/ui/widgets/QtWebEngineHubView.cpp | 186 + .../launcher/ui/widgets/QtWebEngineHubView.h | 50 + .../launcher/ui/widgets/SubTaskProgressBar.cpp | 79 + .../launcher/ui/widgets/SubTaskProgressBar.h | 69 + .../launcher/ui/widgets/SubTaskProgressBar.ui | 100 + .../ui/widgets/VariableSizedImageObject.cpp | 230 + .../launcher/ui/widgets/VariableSizedImageObject.h | 107 + .../launcher/ui/widgets/VersionListView.cpp | 200 + .../launcher/ui/widgets/VersionListView.h | 77 + .../launcher/ui/widgets/VersionSelectWidget.cpp | 291 + .../launcher/ui/widgets/VersionSelectWidget.h | 133 + .../launcher/ui/widgets/WebView2Widget.cpp | 315 + .../launcher/ui/widgets/WebView2Widget.h | 68 + .../projt-launcher/launcher/ui/widgets/WideBar.cpp | 362 + .../projt-launcher/launcher/ui/widgets/WideBar.h | 98 + .../launcher/updater/ExternalUpdater.h | 114 + .../launcher/updater/MacSparkleUpdater.h | 151 + .../launcher/updater/MacSparkleUpdater.mm | 264 + .../launcher/updater/ProjTExternalUpdater.cpp | 430 + .../launcher/updater/ProjTExternalUpdater.h | 125 + .../launcher/updater/projtupdater/ProjTUpdater.cpp | 1784 ++++ .../launcher/updater/projtupdater/ProjTUpdater.h | 199 + .../launcher/updater/projtupdater/ReleaseInfo.cpp | 88 + .../launcher/updater/projtupdater/ReleaseInfo.h | 68 + .../updater/projtupdater/SelectReleaseDialog.ui | 89 + .../updater/projtupdater/UpdaterDialogs.cpp | 211 + .../launcher/updater/projtupdater/UpdaterDialogs.h | 109 + .../updater/projtupdater/updater.exe.manifest | 26 + .../launcher/updater/projtupdater/updater_main.cpp | 64 + archived/projt-launcher/nix/maintainers.nix | 13 + archived/projt-launcher/nix/unwrapped.nix | 226 + archived/projt-launcher/nix/wrapper.nix | 180 + archived/projt-launcher/nuget.config | 7 + archived/projt-launcher/packages.config | 5 + archived/projt-launcher/packaging/arch/PKGBUILD.in | 57 + .../program_info/AdhocSignedApp.entitlements | 12 + .../projt-launcher/program_info/App.entitlements | 10 + .../projt-launcher/program_info/CMakeLists.txt | 98 + .../program_info/LICENSE.instanceicons | 430 + .../program_info/LICENSE.projecttick | 404 + archived/projt-launcher/program_info/ProjT.png | Bin 0 -> 41018 bytes .../org.projecttick.ProjTLauncher_Katman 1.png | Bin 0 -> 9651 bytes .../org.projecttick.ProjTLauncher_Katman 2.png | Bin 0 -> 6560 bytes .../program_info/ProjTLauncher.icon/icon.json | 33 + archived/projt-launcher/program_info/genicons.sh | 70 + archived/projt-launcher/program_info/genmacicon.py | 74 + .../projt-launcher/program_info/gplv3-127x51.png | Bin 0 -> 3471 bytes .../projt-launcher/program_info/instance_icons.svg | 2720 ++++++ .../org.projecttick.ProjTLauncher.Social.svg | 131 + .../org.projecttick.ProjTLauncher.Source.svg | 131 + .../org.projecttick.ProjTLauncher.bigsur.svg | 152 + .../org.projecttick.ProjTLauncher.desktop.in | 13 + ...org.projecttick.ProjTLauncher.logo-darkmode.svg | 173 + .../org.projecttick.ProjTLauncher.logo.source.svg | 173 + .../org.projecttick.ProjTLauncher.logo.svg | 173 + .../org.projecttick.ProjTLauncher.metainfo.xml.in | 63 + .../org.projecttick.ProjTLauncher.mime.xml | 9 + .../program_info/org.projecttick.ProjTLauncher.svg | 181 + .../org.projecttick.ProjTLauncher_256.png | Bin 0 -> 2611 bytes archived/projt-launcher/program_info/portable.txt | 4 + .../program_info/projtlauncher.6.scd.in | 82 + .../projt-launcher/program_info/projtlauncher.icns | Bin 0 -> 77410 bytes .../projt-launcher/program_info/projtlauncher.ico | Bin 0 -> 372782 bytes .../program_info/projtlauncher.manifest.in | 28 + .../program_info/projtlauncher.qrc.in | 7 + .../program_info/projtlauncher.rc.in | 29 + .../projt-launcher/program_info/win_install.nsi.in | 513 + .../scripts/build-cef-from-source.sh | 139 + .../projt-launcher/scripts/build-tomlplusplus.mjs | 110 + archived/projt-launcher/scripts/compress_images.sh | 9 + .../projt-launcher/scripts/gen-cmark-config.sh | 21 + .../projt-launcher/scripts/gen-cmark-export.sh | 30 + .../projt-launcher/scripts/gen-cmark-version.sh | 12 + archived/projt-launcher/scripts/gen-nbt-export.sh | 46 + .../projt-launcher/scripts/gen-qrencode-config.sh | 28 + .../scripts/patch_maintainer_emails.py | 486 + archived/projt-launcher/scripts/syncconfig.sh | 122 + .../projt-launcher/scripts/update-qt-version.sh | 61 + archived/projt-launcher/scripts/update-subtrees.sh | 55 + archived/projt-launcher/shell.nix | 4 + .../tests/ApplicationMessage_test.cpp | 103 + .../tests/BaseInstanceSettings_test.cpp | 55 + archived/projt-launcher/tests/CMakeLists.txt | 192 + archived/projt-launcher/tests/CatPack_test.cpp | 65 + archived/projt-launcher/tests/Commandline_test.cpp | 76 + .../projt-launcher/tests/DataPackParse_test.cpp | 81 + .../projt-launcher/tests/DefaultVariable_test.cpp | 47 + .../tests/ExponentialSeries_test.cpp | 49 + archived/projt-launcher/tests/FileSystem_test.cpp | 899 ++ archived/projt-launcher/tests/Filter_test.cpp | 64 + archived/projt-launcher/tests/GZip_test.cpp | 77 + .../projt-launcher/tests/GradleSpecifier_test.cpp | 95 + archived/projt-launcher/tests/INIFile_test.cpp | 226 + archived/projt-launcher/tests/Index_test.cpp | 52 + .../tests/InstanceCopyPrefs_test.cpp | 74 + archived/projt-launcher/tests/JavaVersion_test.cpp | 157 + archived/projt-launcher/tests/JsonHelpers_test.cpp | 59 + archived/projt-launcher/tests/JsonTypes_test.cpp | 89 + archived/projt-launcher/tests/Json_test.cpp | 211 + archived/projt-launcher/tests/KonamiCode_test.cpp | 107 + .../projt-launcher/tests/LaunchLineRouter_test.cpp | 53 + .../projt-launcher/tests/LaunchLogModel_test.cpp | 103 + .../projt-launcher/tests/LaunchPipeline_test.cpp | 58 + .../tests/LaunchVariableExpander_test.cpp | 59 + archived/projt-launcher/tests/Library_test.cpp | 337 + .../projt-launcher/tests/LogEventParser_test.cpp | 123 + archived/projt-launcher/tests/MMCTime_test.cpp | 76 + .../projt-launcher/tests/MessageLevel_test.cpp | 73 + .../tests/MetaComponentParse_test.cpp | 104 + archived/projt-launcher/tests/ModPlatform_test.cpp | 114 + .../tests/MojangVersionFormat_test.cpp | 56 + .../projt-launcher/tests/NetHeaderProxy_test.cpp | 48 + archived/projt-launcher/tests/NetSink_test.cpp | 147 + archived/projt-launcher/tests/NetUtils_test.cpp | 100 + archived/projt-launcher/tests/Packwiz_test.cpp | 95 + archived/projt-launcher/tests/ParseUtils_test.cpp | 55 + .../tests/ProjTExternalUpdater_test.cpp | 61 + .../tests/ResourceFolderModel_test.cpp | 250 + .../tests/ResourcePackParse_test.cpp | 78 + .../projt-launcher/tests/RuntimeVersion_test.cpp | 73 + .../tests/SeparatorPrefixTree_test.cpp | 168 + .../projt-launcher/tests/ShaderPackParse_test.cpp | 79 + .../tests/StringUtilsHtmlPatch_test.cpp | 38 + .../tests/StringUtilsNaturalCompare_test.cpp | 53 + .../tests/StringUtilsSplitFirst_test.cpp | 48 + .../tests/StringUtilsTruncateUrl_test.cpp | 44 + archived/projt-launcher/tests/StringUtils_test.cpp | 136 + archived/projt-launcher/tests/Task_test.cpp | 324 + .../projt-launcher/tests/TexturePackParse_test.cpp | 76 + .../tests/VersionFilterData_test.cpp | 83 + archived/projt-launcher/tests/Version_test.cpp | 197 + .../projt-launcher/tests/WorldSaveParse_test.cpp | 96 + archived/projt-launcher/tests/XmlLogs_test.cpp | 158 + .../tests/testdata/CatPacks/index.json | 50 + .../DataPackParse/another_test_folder/pack.mcmeta | 6 + .../DataPackParse/test_data_pack_boogaloo.zip | Bin 0 -> 898 bytes .../testdata/DataPackParse/test_folder/pack.mcmeta | 6 + .../FileSystem/FileSystem-test_createShortcut-unix | 6 + .../test_folder/.secret_folder/.secret_file.txt | 1 + .../test_folder/assets/minecraft/textures/blah.txt | 1 + .../testdata/FileSystem/test_folder/pack.mcmeta | 6 + .../tests/testdata/FileSystem/test_folder/pack.nfo | 1 + .../tests/testdata/Libraries/1.9-simple.json | 199 + .../tests/testdata/Libraries/1.9.json | 547 ++ .../tests/testdata/Libraries/codecwav-20101023.jar | 1 + .../tests/testdata/Libraries/lib-native-arch.json | 46 + .../tests/testdata/Libraries/lib-native.json | 56 + .../tests/testdata/Libraries/lib-simple.json | 11 + .../Libraries/testname-testversion-linux-32.jar | 1 + .../tests/testdata/Library/1.9-simple.json | 199 + .../projt-launcher/tests/testdata/Library/1.9.json | 547 ++ .../tests/testdata/Library/codecwav-20101023.jar | 1 + .../tests/testdata/Library/lib-native-arch.json | 46 + .../tests/testdata/Library/lib-native.json | 56 + .../tests/testdata/Library/lib-simple.json | 11 + .../Library/testname-testversion-linux-32.jar | 1 + .../MetaComponentParse/component_basic.json | 8 + .../MetaComponentParse/component_with_extra.json | 21 + .../MetaComponentParse/component_with_format.json | 13 + .../MetaComponentParse/component_with_link.json | 12 + .../MetaComponentParse/component_with_mixed.json | 45 + .../testdata/PackageManifest/1.8.0_202-x64.json | 4667 ++++++++++ .../tests/testdata/PackageManifest/inspect/a/b.txt | 0 .../testdata/PackageManifest/inspect/a/b/b.txt | 1 + .../testdata/PackageManifest/inspect_win/a/b.txt | 0 .../testdata/PackageManifest/inspect_win/a/b/b.txt | 0 .../testdata/Packwiz/borderless-mining.pw.toml | 13 + .../Packwiz/screenshot-to-clipboard-fabric.pw.toml | 13 + .../another_test_folder/pack.mcmeta | 6 + .../testdata/ResourceFolderModel/supercoolmod.jar | 1 + .../test_folder/assets/minecraft/textures/blah.txt | 1 + .../ResourceFolderModel/test_folder/pack.mcmeta | 6 + .../ResourceFolderModel/test_folder/pack.nfo | 1 + .../ResourceFolderModel/test_resource_pack_idk.zip | Bin 0 -> 804 bytes .../Resources/another_test_folder/pack.mcmeta | 6 + .../tests/testdata/Resources/supercoolmod.jar | 1 + .../test_folder/assets/minecraft/textures/blah.txt | 1 + .../testdata/Resources/test_folder/pack.mcmeta | 6 + .../tests/testdata/Resources/test_folder/pack.nfo | 1 + .../testdata/Resources/test_resource_pack_idk.zip | Bin 0 -> 804 bytes .../tests/testdata/ShaderPackParse/shaderpack1.zip | Bin 0 -> 242 bytes .../shaderpack2/shaders/shaders.properties | 0 .../tests/testdata/ShaderPackParse/shaderpack3.zip | Bin 0 -> 128 bytes .../TestLogs/TerraFirmaGreg-Modern-levels.txt | 945 ++ .../TestLogs/TerraFirmaGreg-Modern-xml-levels.txt | 869 ++ .../testdata/TestLogs/vanilla-1.21.5-levels.txt | 25 + .../another_test_texturefolder/pack.txt | 2 + .../TexturePackParse/test_texture_pack_idk.zip | Bin 0 -> 184 bytes .../assets/minecraft/textures/blah.txt | 1 + .../TexturePackParse/test_texturefolder/pack.txt | 1 + .../tests/testdata/Version/test_vectors.txt | 63 + .../testdata/WorldSaveParse/minecraft_save_1.zip | Bin 0 -> 184 bytes .../testdata/WorldSaveParse/minecraft_save_2.zip | Bin 0 -> 352 bytes .../minecraft_save_3/world_3/level.dat | 0 .../minecraft_save_4/saves/world_4/level.dat | 0 .../projt-launcher/tools/generate_todo_report.py | 194 + archived/projt-launcher/tools/get_commits.py | 37 + archived/projt-minicraft-modpack/LICENSE | 24 + .../MiniCraft S1 1.10.2-1.10.2 10.0 Alpha.zip | Bin 0 -> 2049 bytes .../MiniCraft S1 Beta-13.0 13w990a.zip | Bin 0 -> 1784190 bytes .../MiniCraft/MiniCraft S1/MiniCraft S1-12.7.zip | Bin 0 -> 1779377 bytes .../MiniCraft/MiniCraft S1/MiniCraft S1-13.0.0.zip | Bin 0 -> 2092743 bytes .../MiniCraft/MiniCraft S1/MiniCraft S1-13.0.zip | Bin 0 -> 2092217 bytes .../MiniCraft S1/MiniCraft S1-Pre-release 2.zip | Bin 0 -> 1784280 bytes .../MiniCraft S1/minicraft Beta -12.7 12w957g.zip | Bin 0 -> 1177277 bytes .../MiniCraft/MiniCraft S1/minicraft-12.1.5.zip | Bin 0 -> 1816 bytes .../MiniCraft/MiniCraft S1/minicraft-12.3.zip | Bin 0 -> 1014297 bytes .../MiniCraft/MiniCraft S1/minicraft-12.5.1.zip | Bin 0 -> 1177134 bytes .../MiniCraft/MiniCraft S1/minicraft-12.6.2.9.zip | Bin 0 -> 1177210 bytes .../MiniCraft S2/MiniCraft S2-A00051c74C.zip | Bin 0 -> 42509 bytes .../MiniCraft/MiniCraft S2/MiniCraft S2-L3.0.zip | Bin 0 -> 939904 bytes .../MiniCraft S2/MiniCraft S2-R10056a75A.zip | Bin 0 -> 69232 bytes .../MiniCraft S2/MiniCraft S2-R10171a76A.zip | Bin 0 -> 569976 bytes .../MiniCraft/MiniCraft S2/Minicraft S2-N1.0.zip | Bin 0 -> 887068 bytes .../MiniCraft/MiniCraft S2/Minicraft S3-N2.0.zip | Bin 0 -> 907458 bytes .../MiniCraft S3/MiniCraft S3 DEV-1.2.zip | Bin 0 -> 878868 bytes .../MiniCraft/MiniCraft S3/MiniCraft S3-1.0.1.zip | Bin 0 -> 854310 bytes .../MiniCraft/MiniCraft S3/MiniCraft S3-1.0.zip | Bin 0 -> 852923 bytes .../MiniCraft/MiniCraft S3/MiniCraft S3-1.1.1.zip | Bin 0 -> 877783 bytes .../MiniCraft/MiniCraft S3/MiniCraft S3-1.1.zip | Bin 0 -> 871358 bytes .../MiniCraft S3/MiniCraft S3-1.2.0.1.zip | Bin 0 -> 879256 bytes .../MiniCraft S3/MiniCraft S3-1.2.0.2.zip | Bin 0 -> 879277 bytes .../MiniCraft S3/MiniCraft S3-1.2.0.3.zip | Bin 0 -> 879288 bytes .../MiniCraft/MiniCraft S3/MiniCraft S3-1.2.zip | Bin 0 -> 878856 bytes .../MiniCraft S4/MiniCraft S4 ALPHA-0.0.1.zip | Bin 0 -> 466735 bytes .../MiniCraft S4/MiniCraft S4 ALPHA-0.0.2.zip | Bin 0 -> 488604 bytes .../MiniCraft S4/MiniCraft S4 ALPHA-0.0.3.zip | Bin 0 -> 722480 bytes .../MiniCraft S4/MiniCraft S4 BETA-0.1.zip | Bin 0 -> 725406 bytes .../MiniCraft S4/MiniCraft S4 BETA-0.2.1.zip | Bin 0 -> 728484 bytes .../MiniCraft S4/MiniCraft S4 BETA-0.2.zip | Bin 0 -> 725881 bytes .../MiniCraft/MiniCraft S4/MiniCraft S4-1.0.0.zip | Bin 0 -> 505268 bytes .../MiniCraft S4-LASTMAJORRELEASE-2.0.0.zip | Bin 0 -> 518989 bytes archived/projt-minicraft-modpack/README.md | 1 + archived/projt-modpack/.DS_Store | Bin 0 -> 10244 bytes archived/projt-modpack/.gitattributes | 1 + archived/projt-modpack/COPYING.md | 18 + archived/projt-modpack/LICENSE | 674 ++ archived/projt-modpack/ProjT1.png | Bin 0 -> 30449 bytes archived/projt-modpack/ProjT2.png | Bin 0 -> 17739 bytes archived/projt-modpack/ProjT3.png | Bin 0 -> 16269 bytes archived/projt-modpack/README.md | 1 + archived/projt-modpack/affiliate-banner-bg.webp | Bin 0 -> 3032 bytes archived/projt-modpack/affiliate-banner-fg.webp | Bin 0 -> 3214 bytes archived/projt-modpack/bisect-icon.webp | Bin 0 -> 2980 bytes archived/ptlibzippy/.cmake-format.yaml | 245 + archived/ptlibzippy/.gitignore | 57 + archived/ptlibzippy/BUILD.bazel | 134 + archived/ptlibzippy/CMakeLists.txt | 321 + archived/ptlibzippy/COPYING.md | 25 + archived/ptlibzippy/FAQ | 371 + archived/ptlibzippy/INDEX | 67 + archived/ptlibzippy/MODULE.bazel | 9 + archived/ptlibzippy/Makefile.in | 427 + archived/ptlibzippy/README | 117 + archived/ptlibzippy/README-cmake.md | 79 + archived/ptlibzippy/adler32.c | 165 + archived/ptlibzippy/amiga/Makefile.pup | 69 + archived/ptlibzippy/amiga/Makefile.sas | 68 + archived/ptlibzippy/compress.c | 99 + archived/ptlibzippy/configure | 1078 +++ archived/ptlibzippy/contrib/CMakeLists.txt | 67 + archived/ptlibzippy/contrib/README.contrib | 57 + archived/ptlibzippy/contrib/ada/CMakeLists.txt | 217 + archived/ptlibzippy/contrib/ada/buffer_demo.adb | 106 + .../ada/cmake/Modules/CMakeADACompiler.cmake.in | 23 + .../ada/cmake/Modules/CMakeADAInformation.cmake | 133 + .../cmake/Modules/CMakeDetermineADACompiler.cmake | 33 + .../ada/cmake/Modules/CMakeTestADACompiler.cmake | 46 + .../contrib/ada/cmake/binder_helper.cmake | 47 + .../contrib/ada/cmake/compile_helper.cmake | 32 + .../contrib/ada/cmake/exe_link_helper.cmake | 53 + .../contrib/ada/cmake/shared_link_helper.cmake | 52 + .../contrib/ada/cmake/static_link_helper.cmake | 25 + archived/ptlibzippy/contrib/ada/mtest.adb | 156 + archived/ptlibzippy/contrib/ada/ptlib-streams.adb | 225 + archived/ptlibzippy/contrib/ada/ptlib-streams.ads | 114 + archived/ptlibzippy/contrib/ada/ptlib-thin.adb | 142 + archived/ptlibzippy/contrib/ada/ptlib-thin.ads | 450 + archived/ptlibzippy/contrib/ada/ptlib.adb | 701 ++ archived/ptlibzippy/contrib/ada/ptlib.ads | 328 + archived/ptlibzippy/contrib/ada/read.adb | 156 + archived/ptlibzippy/contrib/ada/readme.txt | 65 + archived/ptlibzippy/contrib/ada/test.adb | 463 + archived/ptlibzippy/contrib/ada/zlib.gpr | 20 + archived/ptlibzippy/contrib/blast/CMakeLists.txt | 166 + archived/ptlibzippy/contrib/blast/README | 4 + archived/ptlibzippy/contrib/blast/blast-test.c | 42 + archived/ptlibzippy/contrib/blast/blast.c | 423 + archived/ptlibzippy/contrib/blast/blast.h | 85 + .../ptlibzippy/contrib/blast/blastConfig.cmake.in | 18 + archived/ptlibzippy/contrib/blast/test.pk | Bin 0 -> 8 bytes archived/ptlibzippy/contrib/blast/test.txt | 1 + .../ptlibzippy/contrib/blast/test/CMakeLists.txt | 202 + .../test/add_subdirectory_exclude_test.cmake.in | 27 + .../blast/test/add_subdirectory_test.cmake.in | 25 + .../test/find_package_no_components_test.cmake.in | 24 + .../contrib/blast/test/find_package_test.cmake.in | 24 + .../find_package_wrong_components_test.cmake.in | 24 + archived/ptlibzippy/contrib/blast/tester.cmake | 28 + archived/ptlibzippy/contrib/crc32vx/CMakeLists.txt | 68 + archived/ptlibzippy/contrib/crc32vx/README | 9 + archived/ptlibzippy/contrib/crc32vx/crc32_vx.c | 254 + .../ptlibzippy/contrib/crc32vx/crc32_vx_hooks.h | 9 + archived/ptlibzippy/contrib/delphi/ZLib.pas | 557 ++ archived/ptlibzippy/contrib/delphi/ZLibConst.pas | 11 + archived/ptlibzippy/contrib/delphi/readme.txt | 76 + archived/ptlibzippy/contrib/delphi/zlibd32.mak | 103 + archived/ptlibzippy/contrib/dotzlib/DotZLib.build | 33 + archived/ptlibzippy/contrib/dotzlib/DotZLib.chm | Bin 0 -> 72903 bytes archived/ptlibzippy/contrib/dotzlib/DotZLib.sln | 21 + .../contrib/dotzlib/DotZLib/AssemblyInfo.cs | 58 + .../contrib/dotzlib/DotZLib/ChecksumImpl.cs | 202 + .../contrib/dotzlib/DotZLib/CircularBuffer.cs | 83 + .../contrib/dotzlib/DotZLib/CodecBase.cs | 198 + .../ptlibzippy/contrib/dotzlib/DotZLib/Deflater.cs | 106 + .../ptlibzippy/contrib/dotzlib/DotZLib/DotZLib.cs | 288 + .../contrib/dotzlib/DotZLib/DotZLib.csproj | 141 + .../contrib/dotzlib/DotZLib/GZipStream.cs | 301 + .../ptlibzippy/contrib/dotzlib/DotZLib/Inflater.cs | 105 + .../contrib/dotzlib/DotZLib/UnitTests.cs | 274 + .../ptlibzippy/contrib/dotzlib/LICENSE_1_0.txt | 23 + archived/ptlibzippy/contrib/dotzlib/readme.txt | 61 + .../ptlibzippy/contrib/gcc_gvmat64/CMakeLists.txt | 19 + archived/ptlibzippy/contrib/gcc_gvmat64/gvmat64.S | 570 ++ .../ptlibzippy/contrib/infback9/CMakeLists.txt | 19 + archived/ptlibzippy/contrib/infback9/README | 1 + archived/ptlibzippy/contrib/infback9/infback9.c | 604 ++ archived/ptlibzippy/contrib/infback9/infback9.h | 38 + archived/ptlibzippy/contrib/infback9/inffix9.h | 107 + archived/ptlibzippy/contrib/infback9/inflate9.h | 48 + archived/ptlibzippy/contrib/infback9/inftree9.c | 320 + archived/ptlibzippy/contrib/infback9/inftree9.h | 62 + archived/ptlibzippy/contrib/iostream/test.cpp | 24 + archived/ptlibzippy/contrib/iostream/zfstream.cpp | 329 + archived/ptlibzippy/contrib/iostream/zfstream.h | 128 + archived/ptlibzippy/contrib/iostream2/zstream.h | 306 + .../ptlibzippy/contrib/iostream2/zstream_test.cpp | 25 + .../ptlibzippy/contrib/iostream3/CMakeLists.txt | 200 + archived/ptlibzippy/contrib/iostream3/README | 35 + archived/ptlibzippy/contrib/iostream3/TODO | 17 + .../contrib/iostream3/iostream3Config.cmake.in | 23 + archived/ptlibzippy/contrib/iostream3/test.cc | 50 + .../contrib/iostream3/test/CMakeLists.txt | 207 + .../test/add_subdirectory_exclude_test.cmake.in | 27 + .../iostream3/test/add_subdirectory_test.cmake.in | 25 + .../test/find_package_no_components_test.cmake.in | 24 + .../iostream3/test/find_package_test.cmake.in | 24 + .../find_package_wrong_components_test.cmake.in | 24 + archived/ptlibzippy/contrib/iostream3/zfstream.cc | 479 + archived/ptlibzippy/contrib/iostream3/zfstream.h | 466 + archived/ptlibzippy/contrib/minizip/CMakeLists.txt | 387 + .../ptlibzippy/contrib/minizip/LICENSE.Info-Zip | 58 + archived/ptlibzippy/contrib/minizip/Makefile.am | 46 + .../contrib/minizip/MiniZip64_Changes.txt | 6 + .../ptlibzippy/contrib/minizip/MiniZip64_info.txt | 69 + archived/ptlibzippy/contrib/minizip/configure.ac | 32 + archived/ptlibzippy/contrib/minizip/crypt.h | 125 + archived/ptlibzippy/contrib/minizip/ints.h | 58 + archived/ptlibzippy/contrib/minizip/ioapi.c | 231 + archived/ptlibzippy/contrib/minizip/ioapi.h | 183 + archived/ptlibzippy/contrib/minizip/iowin32.c | 447 + archived/ptlibzippy/contrib/minizip/iowin32.h | 27 + archived/ptlibzippy/contrib/minizip/make_vms.com | 25 + archived/ptlibzippy/contrib/minizip/miniunz.c | 647 ++ archived/ptlibzippy/contrib/minizip/miniunzip.1 | 63 + archived/ptlibzippy/contrib/minizip/minizip.1 | 46 + archived/ptlibzippy/contrib/minizip/minizip.c | 511 + archived/ptlibzippy/contrib/minizip/minizip.pc.in | 13 + archived/ptlibzippy/contrib/minizip/minizip.pc.txt | 13 + .../contrib/minizip/minizipConfig.cmake.in | 27 + archived/ptlibzippy/contrib/minizip/mztools.c | 294 + archived/ptlibzippy/contrib/minizip/mztools.h | 37 + archived/ptlibzippy/contrib/minizip/skipset.h | 367 + .../ptlibzippy/contrib/minizip/test/CMakeLists.txt | 190 + .../test/add_subdirectory_exclude_test.cmake.in | 29 + .../minizip/test/add_subdirectory_test.cmake.in | 28 + .../test/find_package_no_components_test.cmake.in | 26 + .../minizip/test/find_package_test.cmake.in | 26 + .../find_package_wrong_components_test.cmake.in | 26 + .../ptlibzippy/contrib/minizip/test/test_helper.cm | 32 + archived/ptlibzippy/contrib/minizip/unzip.c | 1984 ++++ archived/ptlibzippy/contrib/minizip/unzip.h | 442 + archived/ptlibzippy/contrib/minizip/zip.c | 2246 +++++ archived/ptlibzippy/contrib/minizip/zip.h | 371 + archived/ptlibzippy/contrib/nuget/nuget.csproj | 45 + archived/ptlibzippy/contrib/nuget/nuget.sln | 22 + archived/ptlibzippy/contrib/pascal/example.pas | 599 ++ archived/ptlibzippy/contrib/pascal/readme.txt | 76 + archived/ptlibzippy/contrib/pascal/zlibd32.mak | 103 + archived/ptlibzippy/contrib/pascal/zlibpas.pas | 276 + archived/ptlibzippy/contrib/puff/CMakeLists.txt | 154 + archived/ptlibzippy/contrib/puff/README | 65 + archived/ptlibzippy/contrib/puff/bin-writer.c | 26 + archived/ptlibzippy/contrib/puff/puff.c | 841 ++ archived/ptlibzippy/contrib/puff/puff.h | 36 + .../ptlibzippy/contrib/puff/puffConfig.cmake.in | 18 + archived/ptlibzippy/contrib/puff/pufftest.c | 169 + .../ptlibzippy/contrib/puff/test/CMakeLists.txt | 278 + .../test/add_subdirectory_exclude_test.cmake.in | 26 + .../puff/test/add_subdirectory_test.cmake.in | 25 + .../test/find_package_no_components_test.cmake.in | 24 + .../contrib/puff/test/find_package_test.cmake.in | 24 + .../find_package_wrong_components_test.cmake.in | 24 + .../ptlibzippy/contrib/puff/test/tester-cov.cmake | 58 + archived/ptlibzippy/contrib/puff/test/tester.cmake | 16 + archived/ptlibzippy/contrib/puff/zeros.raw | Bin 0 -> 2517 bytes .../contrib/testptlibzippy/CMakeLists.txt | 44 + .../contrib/testptlibzippy/testptlibzippy.c | 275 + .../contrib/testptlibzippy/testptlibzippy.txt | 10 + archived/ptlibzippy/contrib/vstudio/readme.txt | 22 + .../ptlibzippy/contrib/zlib1-dll/CMakeLists.txt | 196 + archived/ptlibzippy/contrib/zlib1-dll/readme.txt | 21 + archived/ptlibzippy/crc32.c | 984 ++ archived/ptlibzippy/crc32.h | 9446 +++++++++++++++++++ archived/ptlibzippy/deflate.c | 2186 +++++ archived/ptlibzippy/deflate.h | 384 + archived/ptlibzippy/doc/algorithm.txt | 209 + archived/ptlibzippy/doc/crc-doc.1.0.pdf | Bin 0 -> 776142 bytes archived/ptlibzippy/doc/rfc1950.txt | 619 ++ archived/ptlibzippy/doc/rfc1951.txt | 955 ++ archived/ptlibzippy/doc/rfc1952.txt | 675 ++ archived/ptlibzippy/doc/txtvsbin.txt | 107 + archived/ptlibzippy/examples/README.examples | 54 + archived/ptlibzippy/examples/enough.c | 598 ++ archived/ptlibzippy/examples/fitblk.c | 233 + archived/ptlibzippy/examples/gun.c | 703 ++ archived/ptlibzippy/examples/gzappend.c | 505 + archived/ptlibzippy/examples/gzjoin.c | 450 + archived/ptlibzippy/examples/gzlog.c | 1062 +++ archived/ptlibzippy/examples/gzlog.h | 92 + archived/ptlibzippy/examples/gznorm.c | 475 + archived/ptlibzippy/examples/zlib_how.html | 551 ++ archived/ptlibzippy/examples/zpipe.c | 206 + archived/ptlibzippy/examples/zran.c | 547 ++ archived/ptlibzippy/examples/zran.h | 54 + archived/ptlibzippy/gzclose.c | 24 + archived/ptlibzippy/gzlib.c | 610 ++ archived/ptlibzippy/gzread.c | 669 ++ archived/ptlibzippy/gzwrite.c | 701 ++ archived/ptlibzippy/infback.c | 580 ++ archived/ptlibzippy/inffast.c | 322 + archived/ptlibzippy/inffast.h | 12 + archived/ptlibzippy/inffixed.h | 94 + archived/ptlibzippy/inflate.c | 1414 +++ archived/ptlibzippy/inflate.h | 127 + archived/ptlibzippy/inftrees.c | 425 + archived/ptlibzippy/inftrees.h | 65 + archived/ptlibzippy/make_vms.com | 867 ++ archived/ptlibzippy/msdos/Makefile.bor | 115 + archived/ptlibzippy/msdos/Makefile.dj2 | 105 + archived/ptlibzippy/msdos/Makefile.emx | 70 + archived/ptlibzippy/msdos/Makefile.msc | 112 + archived/ptlibzippy/msdos/Makefile.tc | 100 + archived/ptlibzippy/os400/README400 | 48 + archived/ptlibzippy/os400/bndsrc | 144 + archived/ptlibzippy/os400/make.sh | 383 + archived/ptlibzippy/os400/ptlibzippyfixed.rpgle | 578 ++ archived/ptlibzippy/os400/ptlibzippyfree.rpgle | 634 ++ archived/ptlibzippy/ptlibzippy.3 | 150 + archived/ptlibzippy/ptlibzippy.3.pdf | Bin 0 -> 26249 bytes archived/ptlibzippy/ptlibzippy.h | 2069 +++++ archived/ptlibzippy/ptlibzippy.map | 125 + archived/ptlibzippy/ptlibzippy.pc.cmakein | 14 + archived/ptlibzippy/ptlibzippy.pc.in | 14 + archived/ptlibzippy/ptlibzippyConfig.cmake.in | 27 + archived/ptlibzippy/ptlibzippy_pngshim.c | 46 + archived/ptlibzippy/ptzippyconf.h.in | 552 ++ archived/ptlibzippy/ptzippyguts.h | 219 + archived/ptlibzippy/ptzippyutil.c | 313 + archived/ptlibzippy/ptzippyutil.h | 334 + archived/ptlibzippy/qnx/package.qpg | 141 + archived/ptlibzippy/test/CMakeLists.txt | 333 + .../test/add_subdirectory_exclude_test.cmake.in | 29 + .../ptlibzippy/test/add_subdirectory_test.cmake.in | 28 + archived/ptlibzippy/test/example.c | 553 ++ .../test/find_package_no_components_test.cmake.in | 27 + .../ptlibzippy/test/find_package_test.cmake.in | 27 + .../find_package_wrong_components_test.cmake.in | 27 + archived/ptlibzippy/test/infcover.c | 673 ++ archived/ptlibzippy/test/minigzip.c | 593 ++ archived/ptlibzippy/treebuild.xml | 116 + archived/ptlibzippy/trees.c | 1120 +++ archived/ptlibzippy/trees.h | 128 + archived/ptlibzippy/uncompr.c | 101 + archived/ptlibzippy/watcom/watcom_f.mak | 43 + archived/ptlibzippy/watcom/watcom_l.mak | 43 + archived/ptlibzippy/win32/DLL_FAQ.txt | 381 + archived/ptlibzippy/win32/Makefile.bor | 109 + archived/ptlibzippy/win32/Makefile.gcc | 178 + archived/ptlibzippy/win32/Makefile.msc | 159 + archived/ptlibzippy/win32/README-WIN32.txt | 103 + archived/ptlibzippy/win32/VisualC.txt | 3 + archived/ptlibzippy/win32/zlib.def | 104 + archived/ptlibzippy/win32/zlib1.rc | 37 + 2335 files changed, 336261 insertions(+) create mode 100644 archived/projt-launcher/.clang-format create mode 100644 archived/projt-launcher/.clang-format-ignore create mode 100644 archived/projt-launcher/.clang-tidy create mode 100644 archived/projt-launcher/.clusterfuzzlite/Dockerfile create mode 100644 archived/projt-launcher/.clusterfuzzlite/build.sh create mode 100644 archived/projt-launcher/.editorconfig create mode 100644 archived/projt-launcher/.envrc create mode 100644 archived/projt-launcher/.gitattributes create mode 100644 archived/projt-launcher/.gitignore create mode 100644 archived/projt-launcher/.markdownlint.yaml create mode 100644 archived/projt-launcher/.markdownlintignore create mode 100644 archived/projt-launcher/.python-version create mode 100644 archived/projt-launcher/CHANGELOG.md create mode 100644 archived/projt-launcher/CMakeLists.txt create mode 100644 archived/projt-launcher/CMakePresets.json create mode 100644 archived/projt-launcher/CODE_OF_CONDUCT create mode 100644 archived/projt-launcher/COPYING.md create mode 100644 archived/projt-launcher/Containerfile create mode 100644 archived/projt-launcher/MAINTAINERS create mode 100644 archived/projt-launcher/README create mode 100644 archived/projt-launcher/bootstrap/macos/Bootstrap.m create mode 100644 archived/projt-launcher/bootstrap/macos/CMakeLists.txt create mode 100644 archived/projt-launcher/bootstrap/macos/Info.plist.in create mode 100644 archived/projt-launcher/buildconfig/BuildConfig.cpp.in create mode 100644 archived/projt-launcher/buildconfig/BuildConfig.h create mode 100644 archived/projt-launcher/buildconfig/CMakeLists.txt create mode 100644 archived/projt-launcher/buildconfig/Makefile create mode 100644 archived/projt-launcher/ci/code-quality.nix create mode 100644 archived/projt-launcher/ci/code-quality.sh create mode 100644 archived/projt-launcher/ci/codeowners-validator/default.nix create mode 100644 archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch create mode 100644 archived/projt-launcher/ci/codeowners-validator/permissions.patch create mode 100644 archived/projt-launcher/ci/default.nix create mode 100644 archived/projt-launcher/ci/eval/attrpaths.nix create mode 100644 archived/projt-launcher/ci/eval/chunk.nix create mode 100644 archived/projt-launcher/ci/eval/compare/cmp-stats.py create mode 100644 archived/projt-launcher/ci/eval/compare/default.nix create mode 100644 archived/projt-launcher/ci/eval/compare/generate-step-summary.jq create mode 100644 archived/projt-launcher/ci/eval/compare/maintainers.nix create mode 100644 archived/projt-launcher/ci/eval/compare/utils.nix create mode 100644 archived/projt-launcher/ci/eval/default.nix create mode 100644 archived/projt-launcher/ci/eval/diff.nix create mode 100644 archived/projt-launcher/ci/eval/outpaths.nix create mode 100644 archived/projt-launcher/ci/github-script/.editorconfig create mode 100644 archived/projt-launcher/ci/github-script/.gitignore create mode 100644 archived/projt-launcher/ci/github-script/.npmrc create mode 100644 archived/projt-launcher/ci/github-script/backport.js create mode 100644 archived/projt-launcher/ci/github-script/commit-types.json create mode 100644 archived/projt-launcher/ci/github-script/commits.js create mode 100644 archived/projt-launcher/ci/github-script/get-teams.js create mode 100644 archived/projt-launcher/ci/github-script/merge.js create mode 100644 archived/projt-launcher/ci/github-script/package-lock.json create mode 100644 archived/projt-launcher/ci/github-script/package.json create mode 100644 archived/projt-launcher/ci/github-script/prepare.js create mode 100644 archived/projt-launcher/ci/github-script/reviewers.js create mode 100644 archived/projt-launcher/ci/github-script/reviews.js create mode 100644 archived/projt-launcher/ci/github-script/run create mode 100644 archived/projt-launcher/ci/github-script/shell.nix create mode 100644 archived/projt-launcher/ci/github-script/test/commits.test.js create mode 100644 archived/projt-launcher/ci/github-script/withRateLimit.js create mode 100644 archived/projt-launcher/ci/nixpkgs-vet.nix create mode 100644 archived/projt-launcher/ci/nixpkgs-vet.sh create mode 100644 archived/projt-launcher/ci/parse.nix create mode 100644 archived/projt-launcher/ci/pinned.json create mode 100644 archived/projt-launcher/ci/supportedBranches.js create mode 100644 archived/projt-launcher/ci/supportedSystems.json create mode 100644 archived/projt-launcher/ci/supportedVersions.nix create mode 100644 archived/projt-launcher/ci/update-pinned.sh create mode 100644 archived/projt-launcher/cmake/CompilerWarnings.cmake create mode 100644 archived/projt-launcher/cmake/ECMQueryQt.cmake create mode 100644 archived/projt-launcher/cmake/GetGitRevisionDescription.cmake create mode 100644 archived/projt-launcher/cmake/GetGitRevisionDescription.cmake.in create mode 100644 archived/projt-launcher/cmake/GitFunctions.cmake create mode 100644 archived/projt-launcher/cmake/LauncherPackaging.cmake create mode 100644 archived/projt-launcher/cmake/MacOSXBundleInfo.plist.in create mode 100644 archived/projt-launcher/cmake/QtVersionOption.cmake create mode 100644 archived/projt-launcher/cmake/QtVersionlessBackport.cmake create mode 100644 archived/projt-launcher/cmake/findOpenSSLbyNuget.cmake create mode 100644 archived/projt-launcher/cmake/useBZip2.cmake create mode 100644 archived/projt-launcher/cmake/useCMark.cmake create mode 100644 archived/projt-launcher/cmake/useLibpng.cmake create mode 100644 archived/projt-launcher/cmake/useLibqrencode.cmake create mode 100644 archived/projt-launcher/cmake/useMinimalLibs.cmake create mode 100644 archived/projt-launcher/cmake/useMinizip.cmake create mode 100644 archived/projt-launcher/cmake/usePTlibzippy.cmake create mode 100644 archived/projt-launcher/cmake/useQuazip.cmake create mode 100644 archived/projt-launcher/cmake/useTomlplusplus.cmake create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/README.md create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake create mode 100644 archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake create mode 100644 archived/projt-launcher/cmake/vcpkg-triplets/universal-osx.cmake create mode 100644 archived/projt-launcher/default.nix create mode 100644 archived/projt-launcher/docs/APPLE_SILICON_RATIONALE.md create mode 100644 archived/projt-launcher/docs/BUILD_SYSTEM.md create mode 100644 archived/projt-launcher/docs/FUZZING.md create mode 100644 archived/projt-launcher/docs/README.md create mode 100644 archived/projt-launcher/docs/architecture/OVERVIEW.md create mode 100644 archived/projt-launcher/docs/contributing/ARCHITECTURE.md create mode 100644 archived/projt-launcher/docs/contributing/CODE_STYLE.md create mode 100644 archived/projt-launcher/docs/contributing/GETTING_STARTED.md create mode 100644 archived/projt-launcher/docs/contributing/LAUNCHER_TEST_MATRIX.md create mode 100644 archived/projt-launcher/docs/contributing/PROJECT_STRUCTURE.md create mode 100644 archived/projt-launcher/docs/contributing/README.md create mode 100644 archived/projt-launcher/docs/contributing/TESTING.md create mode 100644 archived/projt-launcher/docs/contributing/WORKFLOW.md create mode 100644 archived/projt-launcher/docs/handbook/README.md create mode 100644 archived/projt-launcher/docs/handbook/bot.md create mode 100644 archived/projt-launcher/docs/handbook/bzip2-compiling.md create mode 100644 archived/projt-launcher/docs/handbook/bzip2-testfiles.md create mode 100644 archived/projt-launcher/docs/handbook/bzip2-tests.md create mode 100644 archived/projt-launcher/docs/handbook/bzip2.md create mode 100644 archived/projt-launcher/docs/handbook/ci_support.md create mode 100644 archived/projt-launcher/docs/handbook/cmark.md create mode 100644 archived/projt-launcher/docs/handbook/extra-cmake-modules.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/apis.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/atl-platform.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/custom-commands.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/environment-variables.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/flame-platform.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/ftb-platform.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/index.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/instance-copy.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/instance-version.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/java-settings.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/java-wizard.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/language-settings.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/launcher-settings.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/loader-mods.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/minecraft-settings.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/mod-platform.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/modrinth-platform.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/notes.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/proxy-settings.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/screenshots-management.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/technic-platform.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/tools.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/vanilla-platform.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/worlds.md create mode 100644 archived/projt-launcher/docs/handbook/help-pages/zip-import.md create mode 100644 archived/projt-launcher/docs/handbook/images.md create mode 100644 archived/projt-launcher/docs/handbook/javacheck.md create mode 100644 archived/projt-launcher/docs/handbook/launcherjava.md create mode 100644 archived/projt-launcher/docs/handbook/libnbtplusplus.md create mode 100644 archived/projt-launcher/docs/handbook/libqrencode.md create mode 100644 archived/projt-launcher/docs/handbook/linux-packaging.md create mode 100644 archived/projt-launcher/docs/handbook/nix.md create mode 100644 archived/projt-launcher/docs/handbook/program_info.md create mode 100644 archived/projt-launcher/docs/handbook/ptcieval.md create mode 100644 archived/projt-launcher/docs/handbook/ptcigh.md create mode 100644 archived/projt-launcher/docs/handbook/quazip.md create mode 100644 archived/projt-launcher/docs/handbook/third-party.md create mode 100644 archived/projt-launcher/docs/handbook/tomlplusplus.md create mode 100644 archived/projt-launcher/docs/handbook/website-tomlplusplus.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/development/index.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/development/instructions/index.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/development/instructions/linux.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/development/instructions/macos.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/development/instructions/windows.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/development/translating.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/catpacks.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/command-line-interface.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/controller-support.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/create-instance.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/data-location.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/download-modpacks.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/download-mods.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/index.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/install-of-alternatives.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/installing-java.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/installing-optifine.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/installing-projtlauncher.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/migrating-prismlauncher.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/getting-started/settings.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/apis.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/atl-platform.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/custom-commands.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/environment-variables.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/flame-platform.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/ftb-platform.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/index.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/instance-copy.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/instance-version.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/java-settings.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/java-wizard.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/language-settings.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/launcher-settings.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/loader-mods.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/minecraft-settings.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/mod-platform.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/modrinth-platform.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/notes.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/proxy-settings.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/screenshots-management.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/technic-platform.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/tools.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/vanilla-platform.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/worlds.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/help-pages/zip-import.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/overview/code-of-conduct.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/overview/copying.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/overview/faq.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/overview/feedback-bugs.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/overview/frequent-issues.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/overview/index.md create mode 100644 archived/projt-launcher/docs/handbook/wiki/wiki.json create mode 100644 archived/projt-launcher/docs/handbook/workflows.md create mode 100644 archived/projt-launcher/docs/handbook/zlib.md create mode 100644 archived/projt-launcher/docs/packaging/README.md create mode 100644 archived/projt-launcher/docs/packaging/os-specific/linux/flathub/flathubpackage.yml create mode 100644 archived/projt-launcher/docs/packaging/os-specific/linux/flathub/projtlauncher create mode 100644 archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher-unwrapped.nix create mode 100644 archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher.nix create mode 100644 archived/projt-launcher/docs/packaging/os-specific/macos/homebrew/projtlauncher.rb create mode 100644 archived/projt-launcher/flake.lock create mode 100644 archived/projt-launcher/flake.nix create mode 100644 archived/projt-launcher/flatpak/.editorconfig create mode 100644 archived/projt-launcher/flatpak/.gitignore create mode 100644 archived/projt-launcher/flatpak/.gitmodules create mode 100644 archived/projt-launcher/flatpak/README.md create mode 100644 archived/projt-launcher/flatpak/modules/flite.yml create mode 100644 archived/projt-launcher/flatpak/modules/glfw.yml create mode 100644 archived/projt-launcher/flatpak/modules/glxinfo.yml create mode 100644 archived/projt-launcher/flatpak/modules/inih.yml create mode 100644 archived/projt-launcher/flatpak/modules/xrandr.yml create mode 100644 archived/projt-launcher/flatpak/org.projecttick.ProjTLauncher.yml create mode 100644 archived/projt-launcher/flatpak/patches/0001-Ez-peazy.flite.patch create mode 100644 archived/projt-launcher/flatpak/patches/0001-Wayland-Partially-implement-glfwSetCursorPos.patch create mode 100644 archived/projt-launcher/flatpak/patches/0002-Wayland-Implement-glfwSetWindowIcon.patch create mode 100644 archived/projt-launcher/flatpak/patches/0003-proceed-even-though-no-window-icon-support-on-waylan.patch create mode 100644 archived/projt-launcher/flatpak/pubkey.asc create mode 100644 archived/projt-launcher/flatpak/scripts/prime-run create mode 100644 archived/projt-launcher/flatpak/scripts/projtlauncher create mode 100644 archived/projt-launcher/flatpak/static/index.html create mode 100644 archived/projt-launcher/flatpak/static/projtlauncher-nightly.flatpakref create mode 100644 archived/projt-launcher/flatpak/static/projtlauncher.flatpakrepo create mode 100644 archived/projt-launcher/fuzz/CMakeLists.txt create mode 100644 archived/projt-launcher/fuzz/fuzz_gzip.cpp create mode 100644 archived/projt-launcher/fuzz/fuzz_nbt_reader.cpp create mode 100644 archived/projt-launcher/fuzz/fuzz_qjson_parse.cpp create mode 100644 archived/projt-launcher/fuzz/fuzz_separator_prefix_tree.cpp create mode 100644 archived/projt-launcher/garnix.yaml create mode 100644 archived/projt-launcher/launcher/Application.cpp create mode 100644 archived/projt-launcher/launcher/Application.h create mode 100644 archived/projt-launcher/launcher/ApplicationMessage.cpp create mode 100644 archived/projt-launcher/launcher/ApplicationMessage.h create mode 100644 archived/projt-launcher/launcher/BaseInstaller.cpp create mode 100644 archived/projt-launcher/launcher/BaseInstaller.h create mode 100644 archived/projt-launcher/launcher/BaseInstance.cpp create mode 100644 archived/projt-launcher/launcher/BaseInstance.h create mode 100644 archived/projt-launcher/launcher/BaseVersion.h create mode 100644 archived/projt-launcher/launcher/BaseVersionList.cpp create mode 100644 archived/projt-launcher/launcher/BaseVersionList.h create mode 100644 archived/projt-launcher/launcher/CMakeLists.txt create mode 100644 archived/projt-launcher/launcher/CefRuntime.cpp create mode 100644 archived/projt-launcher/launcher/CefRuntime.h create mode 100644 archived/projt-launcher/launcher/Commandline.cpp create mode 100644 archived/projt-launcher/launcher/Commandline.h create mode 100644 archived/projt-launcher/launcher/DataMigrationTask.cpp create mode 100644 archived/projt-launcher/launcher/DataMigrationTask.h create mode 100644 archived/projt-launcher/launcher/DefaultVariable.h create mode 100644 archived/projt-launcher/launcher/DesktopServices.cpp create mode 100644 archived/projt-launcher/launcher/DesktopServices.h create mode 100644 archived/projt-launcher/launcher/Exception.h create mode 100644 archived/projt-launcher/launcher/ExponentialSeries.h create mode 100644 archived/projt-launcher/launcher/FileSystem.cpp create mode 100644 archived/projt-launcher/launcher/FileSystem.h create mode 100644 archived/projt-launcher/launcher/Filter.h create mode 100644 archived/projt-launcher/launcher/GZip.cpp create mode 100644 archived/projt-launcher/launcher/GZip.h create mode 100644 archived/projt-launcher/launcher/HardwareInfo.cpp create mode 100644 archived/projt-launcher/launcher/HardwareInfo.h create mode 100644 archived/projt-launcher/launcher/InstanceCopyPrefs.cpp create mode 100644 archived/projt-launcher/launcher/InstanceCopyPrefs.h create mode 100644 archived/projt-launcher/launcher/InstanceCopyTask.cpp create mode 100644 archived/projt-launcher/launcher/InstanceCopyTask.h create mode 100644 archived/projt-launcher/launcher/InstanceCreationTask.cpp create mode 100644 archived/projt-launcher/launcher/InstanceCreationTask.h create mode 100644 archived/projt-launcher/launcher/InstanceDirUpdate.cpp create mode 100644 archived/projt-launcher/launcher/InstanceDirUpdate.h create mode 100644 archived/projt-launcher/launcher/InstanceImportTask.cpp create mode 100644 archived/projt-launcher/launcher/InstanceImportTask.h create mode 100644 archived/projt-launcher/launcher/InstanceList.cpp create mode 100644 archived/projt-launcher/launcher/InstanceList.h create mode 100644 archived/projt-launcher/launcher/InstancePageProvider.h create mode 100644 archived/projt-launcher/launcher/InstanceTask.cpp create mode 100644 archived/projt-launcher/launcher/InstanceTask.h create mode 100644 archived/projt-launcher/launcher/JavaCommon.cpp create mode 100644 archived/projt-launcher/launcher/JavaCommon.h create mode 100644 archived/projt-launcher/launcher/Json.cpp create mode 100644 archived/projt-launcher/launcher/Json.h create mode 100644 archived/projt-launcher/launcher/Kconfig create mode 100644 archived/projt-launcher/launcher/KonamiCode.cpp create mode 100644 archived/projt-launcher/launcher/KonamiCode.h create mode 100644 archived/projt-launcher/launcher/LaunchController.cpp create mode 100644 archived/projt-launcher/launcher/LaunchController.h create mode 100644 archived/projt-launcher/launcher/LaunchMode.h create mode 100755 archived/projt-launcher/launcher/Launcher.in create mode 100644 archived/projt-launcher/launcher/LoggedProcess.cpp create mode 100644 archived/projt-launcher/launcher/LoggedProcess.h create mode 100644 archived/projt-launcher/launcher/MMCTime.cpp create mode 100644 archived/projt-launcher/launcher/MMCTime.h create mode 100644 archived/projt-launcher/launcher/MMCZip.cpp create mode 100644 archived/projt-launcher/launcher/MMCZip.h create mode 100644 archived/projt-launcher/launcher/MTPixmapCache.h create mode 100644 archived/projt-launcher/launcher/MangoHud.cpp create mode 100644 archived/projt-launcher/launcher/MangoHud.h create mode 100644 archived/projt-launcher/launcher/Markdown.cpp create mode 100644 archived/projt-launcher/launcher/Markdown.h create mode 100644 archived/projt-launcher/launcher/MessageLevel.cpp create mode 100644 archived/projt-launcher/launcher/MessageLevel.h create mode 100644 archived/projt-launcher/launcher/NullInstance.h create mode 100644 archived/projt-launcher/launcher/PSaveFile.h create mode 100644 archived/projt-launcher/launcher/PngReader.cpp create mode 100644 archived/projt-launcher/launcher/PngReader.h create mode 100644 archived/projt-launcher/launcher/ProblemProvider.h create mode 100644 archived/projt-launcher/launcher/QObjectPtr.h create mode 100644 archived/projt-launcher/launcher/QVariantUtils.h create mode 100644 archived/projt-launcher/launcher/RWStorage.h create mode 100644 archived/projt-launcher/launcher/RecursiveFileSystemWatcher.cpp create mode 100644 archived/projt-launcher/launcher/RecursiveFileSystemWatcher.h create mode 100644 archived/projt-launcher/launcher/ResourceDownloadTask.cpp create mode 100644 archived/projt-launcher/launcher/ResourceDownloadTask.h create mode 100644 archived/projt-launcher/launcher/RuntimeContext.h create mode 100644 archived/projt-launcher/launcher/SeparatorPrefixTree.h create mode 100644 archived/projt-launcher/launcher/StringUtils.cpp create mode 100644 archived/projt-launcher/launcher/StringUtils.h create mode 100644 archived/projt-launcher/launcher/SysInfo.cpp create mode 100644 archived/projt-launcher/launcher/SysInfo.h create mode 100644 archived/projt-launcher/launcher/Untar.cpp create mode 100644 archived/projt-launcher/launcher/Untar.h create mode 100644 archived/projt-launcher/launcher/Usable.h create mode 100644 archived/projt-launcher/launcher/Version.cpp create mode 100644 archived/projt-launcher/launcher/Version.h create mode 100644 archived/projt-launcher/launcher/VersionProxyModel.cpp create mode 100644 archived/projt-launcher/launcher/VersionProxyModel.h create mode 100644 archived/projt-launcher/launcher/WatchLock.h create mode 100644 archived/projt-launcher/launcher/console/Console.hpp create mode 100644 archived/projt-launcher/launcher/console/ConsoleStub.cpp create mode 100644 archived/projt-launcher/launcher/console/WindowsConsole.cpp create mode 100644 archived/projt-launcher/launcher/console/WindowsConsole.hpp create mode 100644 archived/projt-launcher/launcher/filelink/FileLink.cpp create mode 100644 archived/projt-launcher/launcher/filelink/FileLink.hpp create mode 100644 archived/projt-launcher/launcher/filelink/filelink.exe.manifest create mode 100644 archived/projt-launcher/launcher/filelink/filelink_main.cpp create mode 100644 archived/projt-launcher/launcher/icons/IconEntry.cpp create mode 100644 archived/projt-launcher/launcher/icons/IconEntry.hpp create mode 100644 archived/projt-launcher/launcher/icons/IconList.cpp create mode 100644 archived/projt-launcher/launcher/icons/IconList.hpp create mode 100644 archived/projt-launcher/launcher/icons/IconUtils.cpp create mode 100644 archived/projt-launcher/launcher/icons/IconUtils.hpp create mode 100644 archived/projt-launcher/launcher/java/core/RuntimeInstall.cpp create mode 100644 archived/projt-launcher/launcher/java/core/RuntimeInstall.hpp create mode 100644 archived/projt-launcher/launcher/java/core/RuntimePackage.cpp create mode 100644 archived/projt-launcher/launcher/java/core/RuntimePackage.hpp create mode 100644 archived/projt-launcher/launcher/java/core/RuntimeVersion.cpp create mode 100644 archived/projt-launcher/launcher/java/core/RuntimeVersion.hpp create mode 100644 archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.cpp create mode 100644 archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.hpp create mode 100644 archived/projt-launcher/launcher/java/download/RuntimeLinkTask.cpp create mode 100644 archived/projt-launcher/launcher/java/download/RuntimeLinkTask.hpp create mode 100644 archived/projt-launcher/launcher/java/download/RuntimeManifestTask.cpp create mode 100644 archived/projt-launcher/launcher/java/download/RuntimeManifestTask.hpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeCatalog.cpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeCatalog.hpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeEnvironment.cpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeEnvironment.hpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeProbeTask.cpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeProbeTask.hpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeScanner.cpp create mode 100644 archived/projt-launcher/launcher/java/services/RuntimeScanner.hpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchLineRouter.cpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchLineRouter.hpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchLogModel.cpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchLogModel.hpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchPipeline.cpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchPipeline.hpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchStage.cpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchStage.hpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchVariableExpander.cpp create mode 100644 archived/projt-launcher/launcher/launch/LaunchVariableExpander.hpp create mode 100644 archived/projt-launcher/launcher/launch/TaskBridgeStage.cpp create mode 100644 archived/projt-launcher/launcher/launch/TaskBridgeStage.hpp create mode 100644 archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.cpp create mode 100644 archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.hpp create mode 100644 archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.cpp create mode 100644 archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.hpp create mode 100644 archived/projt-launcher/launcher/launch/steps/LogMessageStep.cpp create mode 100644 archived/projt-launcher/launcher/launch/steps/LogMessageStep.hpp create mode 100644 archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.cpp create mode 100644 archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.hpp create mode 100644 archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.cpp create mode 100644 archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.hpp create mode 100644 archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.cpp create mode 100644 archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.hpp create mode 100644 archived/projt-launcher/launcher/logs/LogEventParser.cpp create mode 100644 archived/projt-launcher/launcher/logs/LogEventParser.hpp create mode 100644 archived/projt-launcher/launcher/logs/LogRedactor.cpp create mode 100644 archived/projt-launcher/launcher/logs/LogRedactor.hpp create mode 100644 archived/projt-launcher/launcher/main.cpp create mode 100644 archived/projt-launcher/launcher/meta/BaseEntity.cpp create mode 100644 archived/projt-launcher/launcher/meta/BaseEntity.hpp create mode 100644 archived/projt-launcher/launcher/meta/Index.cpp create mode 100644 archived/projt-launcher/launcher/meta/Index.hpp create mode 100644 archived/projt-launcher/launcher/meta/JsonFormat.cpp create mode 100644 archived/projt-launcher/launcher/meta/JsonFormat.hpp create mode 100644 archived/projt-launcher/launcher/meta/Version.cpp create mode 100644 archived/projt-launcher/launcher/meta/Version.hpp create mode 100644 archived/projt-launcher/launcher/meta/VersionList.cpp create mode 100644 archived/projt-launcher/launcher/meta/VersionList.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/Agent.h create mode 100644 archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/AssetsUtils.h create mode 100644 archived/projt-launcher/launcher/minecraft/BackupManager.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/BackupManager.h create mode 100644 archived/projt-launcher/launcher/minecraft/Component.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/Component.h create mode 100644 archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.h create mode 100644 archived/projt-launcher/launcher/minecraft/ComponentUpdateTask_p.h create mode 100644 archived/projt-launcher/launcher/minecraft/GradleSpecifier.h create mode 100644 archived/projt-launcher/launcher/minecraft/LaunchProfile.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/LaunchProfile.h create mode 100644 archived/projt-launcher/launcher/minecraft/Library.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/Library.h create mode 100644 archived/projt-launcher/launcher/minecraft/Logging.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/Logging.h create mode 100644 archived/projt-launcher/launcher/minecraft/MinecraftInstance.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/MinecraftInstance.h create mode 100644 archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.h create mode 100644 archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.h create mode 100644 archived/projt-launcher/launcher/minecraft/MojangDownloadInfo.h create mode 100644 archived/projt-launcher/launcher/minecraft/MojangVersionFormat.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/MojangVersionFormat.h create mode 100644 archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.h create mode 100644 archived/projt-launcher/launcher/minecraft/PackProfile.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/PackProfile.h create mode 100644 archived/projt-launcher/launcher/minecraft/PackProfile_p.h create mode 100644 archived/projt-launcher/launcher/minecraft/ParseUtils.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/ParseUtils.h create mode 100644 archived/projt-launcher/launcher/minecraft/ProfileUtils.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/ProfileUtils.h create mode 100644 archived/projt-launcher/launcher/minecraft/Rule.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/Rule.h create mode 100644 archived/projt-launcher/launcher/minecraft/ShortcutUtils.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/ShortcutUtils.h create mode 100644 archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.h create mode 100644 archived/projt-launcher/launcher/minecraft/VersionFile.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/VersionFile.h create mode 100644 archived/projt-launcher/launcher/minecraft/VersionFilterData.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/VersionFilterData.h create mode 100644 archived/projt-launcher/launcher/minecraft/World.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/World.h create mode 100644 archived/projt-launcher/launcher/minecraft/WorldList.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/WorldList.h create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AccountData.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AccountData.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AccountList.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AccountList.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AuthFlow.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AuthFlow.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AuthSession.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/AuthSession.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/Parsers.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/Parsers.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/Credentials.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/Step.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/Steps.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/DataPack.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/DataPack.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/MetadataHandler.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/Mod.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/Mod.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ModDetails.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/Resource.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/Resource.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ResourcePack.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ResourcePack.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ShaderPack.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ShaderPack.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/ShaderPackFolderModel.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/TexturePack.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/TexturePack.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/WorldSave.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/WorldSave.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.hpp create mode 100644 archived/projt-launcher/launcher/minecraft/skins/CapeChange.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/skins/CapeChange.h create mode 100644 archived/projt-launcher/launcher/minecraft/skins/CapeListModel.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/skins/CapeListModel.h create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinDelete.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinDelete.h create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinList.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinList.h create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinModel.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinModel.h create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinUpload.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/skins/SkinUpload.h create mode 100644 archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.h create mode 100644 archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.h create mode 100644 archived/projt-launcher/launcher/minecraft/update/FoldersTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/update/FoldersTask.h create mode 100644 archived/projt-launcher/launcher/minecraft/update/LibrariesTask.cpp create mode 100644 archived/projt-launcher/launcher/minecraft/update/LibrariesTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/ModIndex.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/ModIndex.h create mode 100644 archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/ResourceAPI.h create mode 100644 archived/projt-launcher/launcher/modplatform/ResourceType.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/ResourceType.h create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/flame/PackManifest.h create mode 100644 archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h create mode 100644 archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h create mode 100644 archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h create mode 100644 archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h create mode 100644 archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h create mode 100644 archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h create mode 100644 archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h create mode 100644 archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h create mode 100644 archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h create mode 100644 archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp create mode 100644 archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h create mode 100644 archived/projt-launcher/launcher/net/ApiDownload.cpp create mode 100644 archived/projt-launcher/launcher/net/ApiDownload.h create mode 100644 archived/projt-launcher/launcher/net/ApiHeaderProxy.h create mode 100644 archived/projt-launcher/launcher/net/ApiUpload.cpp create mode 100644 archived/projt-launcher/launcher/net/ApiUpload.h create mode 100644 archived/projt-launcher/launcher/net/ByteArraySink.h create mode 100644 archived/projt-launcher/launcher/net/ChecksumValidator.h create mode 100644 archived/projt-launcher/launcher/net/Download.cpp create mode 100644 archived/projt-launcher/launcher/net/Download.h create mode 100644 archived/projt-launcher/launcher/net/FileSink.cpp create mode 100644 archived/projt-launcher/launcher/net/FileSink.h create mode 100644 archived/projt-launcher/launcher/net/HeaderProxy.h create mode 100644 archived/projt-launcher/launcher/net/HttpMetaCache.cpp create mode 100644 archived/projt-launcher/launcher/net/HttpMetaCache.h create mode 100644 archived/projt-launcher/launcher/net/Logging.cpp create mode 100644 archived/projt-launcher/launcher/net/Logging.h create mode 100644 archived/projt-launcher/launcher/net/MetaCacheSink.cpp create mode 100644 archived/projt-launcher/launcher/net/MetaCacheSink.h create mode 100644 archived/projt-launcher/launcher/net/Mode.h create mode 100644 archived/projt-launcher/launcher/net/NetJob.cpp create mode 100644 archived/projt-launcher/launcher/net/NetJob.h create mode 100644 archived/projt-launcher/launcher/net/NetRequest.cpp create mode 100644 archived/projt-launcher/launcher/net/NetRequest.h create mode 100644 archived/projt-launcher/launcher/net/NetUtils.h create mode 100644 archived/projt-launcher/launcher/net/PasteUpload.cpp create mode 100644 archived/projt-launcher/launcher/net/PasteUpload.h create mode 100644 archived/projt-launcher/launcher/net/RawHeaderProxy.h create mode 100644 archived/projt-launcher/launcher/net/Sink.h create mode 100644 archived/projt-launcher/launcher/net/Upload.cpp create mode 100644 archived/projt-launcher/launcher/net/Upload.h create mode 100644 archived/projt-launcher/launcher/net/Validator.h create mode 100644 archived/projt-launcher/launcher/news/NewsChecker.cpp create mode 100644 archived/projt-launcher/launcher/news/NewsChecker.h create mode 100644 archived/projt-launcher/launcher/news/NewsEntry.cpp create mode 100644 archived/projt-launcher/launcher/news/NewsEntry.h create mode 100644 archived/projt-launcher/launcher/qtlogging.ini create mode 100644 archived/projt-launcher/launcher/resources/OSX/OSX.qrc create mode 100644 archived/projt-launcher/launcher/resources/OSX/index.theme create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/OSX/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/assets/underconstruction.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/backgrounds.qrc create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/kitteh-bday.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/kitteh-spooky.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/kitteh-xmas.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/kitteh.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory-bday.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory-flat-bday.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory-flat-spooky.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory-flat-xmas.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory-flat.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory-spooky.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory-xmas.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/rory.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/teawie-bday.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/teawie-spooky.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/teawie-xmas.png create mode 100644 archived/projt-launcher/launcher/resources/backgrounds/teawie.png create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/breeze_dark.qrc create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/index.theme create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/discord.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/matrix.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/reddit-alien.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_dark/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/breeze_light.qrc create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/index.theme create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/discord.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/matrix.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/reddit-alien.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/breeze_light/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/documents/credits.html create mode 100644 archived/projt-launcher/launcher/resources/documents/documents.qrc create mode 100644 archived/projt-launcher/launcher/resources/documents/manifesto.md create mode 100644 archived/projt-launcher/launcher/resources/flat/flat.qrc create mode 100644 archived/projt-launcher/launcher/resources/flat/index.theme create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/cat.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/discord.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/multimc.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/packages.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/quickmods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/reddit-alien.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/screenshot-placeholder.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/star.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/status-running.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/flat/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/flat_white.qrc create mode 100644 archived/projt-launcher/launcher/resources/flat_white/index.theme create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/cat.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/discord.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/multimc.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/packages.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/quickmods.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/reddit-alien.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/screenshot-placeholder.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/star.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/status-running.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/flat_white/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/iOS.qrc create mode 100644 archived/projt-launcher/launcher/resources/iOS/index.theme create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/multimc.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/iOS/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/chicken_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/creeper_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/flame_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/forge.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_glow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/gear_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/herobrine_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/infinity_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/liteloader.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/magitech_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/meat_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/netherstar_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/skeleton_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/instances/steve_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/shaderpacks.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/128x128/unknown_server.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/bug.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/cat.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/centralmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/copy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/coremods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/help.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/instance-settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/jarmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/loadermods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/log.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/minecraft.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/new.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/news.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/noaccount.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/patreon.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/refresh.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/resourcepacks.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/screenshots.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/star.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/status-bad.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/status-good.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/status-running.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/status-yellow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/viewfolder.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/16x16/worlds.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/bug.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/cat.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/centralmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/copy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/help.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/instance-settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/news.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/patreon.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/refresh.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/screenshots.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/status-bad.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/status-good.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/status-running.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/status-yellow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/viewfolder.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/22x22/worlds.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/cat.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/coremods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/jarmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/loadermods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/log.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/minecraft.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/noaccount.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/patreon.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/resourcepacks.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/star.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/status-bad.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/status-good.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/status-running.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/24x24/status-yellow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/256x256/minecraft.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/bug.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/cat.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/centralmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/copy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/coremods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/help.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instance-settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/brick_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/chicken_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/creeper_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/diamond_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/dirt_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_glow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/gear_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/gold_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/grass_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/herobrine_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/infinity_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/iron_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/magitech_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/meat_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/netherstar_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/planks_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/skeleton_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/steve_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/stone_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/instances/tnt_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/jarmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/loadermods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/log.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/minecraft.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/news.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/noaccount.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/patreon.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/refresh.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/resourcepacks.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/screenshots.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/star.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/status-bad.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/status-good.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/status-running.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/status-yellow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/viewfolder.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/32x32/worlds.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/bug.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/cat.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/centralmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/copy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/help.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/instance-settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/log.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/minecraft.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/news.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/noaccount.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/patreon.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/refresh.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/screenshots.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/star.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/status-bad.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/status-good.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/status-running.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/status-yellow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/viewfolder.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/48x48/worlds.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/50x50/instances/enderman_legacy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/bug.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/cat.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/centralmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/copy.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/coremods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/help.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/instance-settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/jarmods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/loadermods.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/log.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/news.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/patreon.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/refresh.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/resourcepacks.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/screenshots.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/settings.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/star.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/status-bad.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/status-good.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/status-running.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/status-yellow.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/viewfolder.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/64x64/worlds.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/8x8/noaccount.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/index.theme create mode 100644 archived/projt-launcher/launcher/resources/multimc/multimc.qrc create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/about.svg.license create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/adoptium.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher-placeholder.png create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/azul.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg.license create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/discord.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee_legacy.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/brick.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/chicken.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/creeper.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/diamond.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/dirt.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderman.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderpearl.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/fabricmc.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/flame.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox_legacy.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/ftb_logo.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/gear.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/gold.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/grass.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/herobrine.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/iron.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/magitech.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/meat.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/modrinth.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/neoforged.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/netherstar.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/planks.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/projtlauncher.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/quiltmc.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/skeleton.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/squarecreeper.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/steve.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/stone.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/instances/tnt.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/launcher.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/matrix.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/mojang.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/new.svg.license create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/openj9.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/reddit-alien.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/screenshot-placeholder.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/status-running.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/technic.svg create mode 100644 archived/projt-launcher/launcher/resources/multimc/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/index.theme create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/pe_blue.qrc create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_blue/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/index.theme create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/pe_colored.qrc create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_colored/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/index.theme create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/pe_dark.qrc create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_dark/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/index.theme create mode 100644 archived/projt-launcher/launcher/resources/pe_light/pe_light.qrc create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/about.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/accounts.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/appearance.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/bug.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/centralmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/checkupdate.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/copy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/coremods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/custom-commands.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/datapacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/delete.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/export.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/externaltools.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/help.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/instance-settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/jarmods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/java.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/language.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/launch.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/loadermods.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/log.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/minecraft.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/new.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/news.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/notes.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/patreon.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/proxy.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/refresh.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/rename.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/resourcepacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/screenshots.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/server.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/settings.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/shaderpacks.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/shortcut.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/status-bad.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/status-good.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/status-yellow.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/tag.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/viewfolder.svg create mode 100644 archived/projt-launcher/launcher/resources/pe_light/scalable/worlds.svg create mode 100644 archived/projt-launcher/launcher/resources/shaders/fshader.glsl create mode 100644 archived/projt-launcher/launcher/resources/shaders/shaders.qrc create mode 100644 archived/projt-launcher/launcher/resources/shaders/vshader_skin_background.glsl create mode 100644 archived/projt-launcher/launcher/resources/shaders/vshader_skin_model.glsl create mode 100644 archived/projt-launcher/launcher/resources/sources/burfcat_hat.png create mode 100644 archived/projt-launcher/launcher/resources/sources/cattiversary.xcf create mode 100644 archived/projt-launcher/launcher/resources/sources/clucker.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/creeper.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/enderpearl.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/flame.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/ftb-glow.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/ftb-logo.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/gear.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/herobrine.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/magitech.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/meat.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/netherstar.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/pskeleton.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/skeleton.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/squarecreeper.svg create mode 100644 archived/projt-launcher/launcher/resources/sources/steve.svg create mode 100644 archived/projt-launcher/launcher/screenshots/ImgurAlbumCreation.cpp create mode 100644 archived/projt-launcher/launcher/screenshots/ImgurAlbumCreation.h create mode 100644 archived/projt-launcher/launcher/screenshots/ImgurUpload.cpp create mode 100644 archived/projt-launcher/launcher/screenshots/ImgurUpload.h create mode 100644 archived/projt-launcher/launcher/screenshots/Screenshot.h create mode 100644 archived/projt-launcher/launcher/settings/INIFile.cpp create mode 100644 archived/projt-launcher/launcher/settings/INIFile.h create mode 100644 archived/projt-launcher/launcher/settings/INISettingsObject.cpp create mode 100644 archived/projt-launcher/launcher/settings/INISettingsObject.h create mode 100644 archived/projt-launcher/launcher/settings/OverrideSetting.cpp create mode 100644 archived/projt-launcher/launcher/settings/OverrideSetting.h create mode 100644 archived/projt-launcher/launcher/settings/PassthroughSetting.cpp create mode 100644 archived/projt-launcher/launcher/settings/PassthroughSetting.h create mode 100644 archived/projt-launcher/launcher/settings/Setting.cpp create mode 100644 archived/projt-launcher/launcher/settings/Setting.h create mode 100644 archived/projt-launcher/launcher/settings/SettingsObject.cpp create mode 100644 archived/projt-launcher/launcher/settings/SettingsObject.h create mode 100644 archived/projt-launcher/launcher/tasks/ConcurrentTask.cpp create mode 100644 archived/projt-launcher/launcher/tasks/ConcurrentTask.h create mode 100644 archived/projt-launcher/launcher/tasks/MultipleOptionsTask.cpp create mode 100644 archived/projt-launcher/launcher/tasks/MultipleOptionsTask.h create mode 100644 archived/projt-launcher/launcher/tasks/SequentialTask.cpp create mode 100644 archived/projt-launcher/launcher/tasks/SequentialTask.h create mode 100644 archived/projt-launcher/launcher/tasks/Task.cpp create mode 100644 archived/projt-launcher/launcher/tasks/Task.h create mode 100644 archived/projt-launcher/launcher/tools/BaseExternalTool.cpp create mode 100644 archived/projt-launcher/launcher/tools/BaseExternalTool.h create mode 100644 archived/projt-launcher/launcher/tools/BaseProfiler.cpp create mode 100644 archived/projt-launcher/launcher/tools/BaseProfiler.h create mode 100644 archived/projt-launcher/launcher/tools/GenericProfiler.cpp create mode 100644 archived/projt-launcher/launcher/tools/GenericProfiler.h create mode 100644 archived/projt-launcher/launcher/tools/JProfiler.cpp create mode 100644 archived/projt-launcher/launcher/tools/JProfiler.h create mode 100644 archived/projt-launcher/launcher/tools/JVisualVM.cpp create mode 100644 archived/projt-launcher/launcher/tools/JVisualVM.h create mode 100644 archived/projt-launcher/launcher/tools/MCEditTool.cpp create mode 100644 archived/projt-launcher/launcher/tools/MCEditTool.h create mode 100644 archived/projt-launcher/launcher/translations/POTranslator.cpp create mode 100644 archived/projt-launcher/launcher/translations/POTranslator.h create mode 100644 archived/projt-launcher/launcher/translations/TranslationsModel.cpp create mode 100644 archived/projt-launcher/launcher/translations/TranslationsModel.h create mode 100644 archived/projt-launcher/launcher/ui/GuiUtil.cpp create mode 100644 archived/projt-launcher/launcher/ui/GuiUtil.h create mode 100644 archived/projt-launcher/launcher/ui/InstanceWindow.cpp create mode 100644 archived/projt-launcher/launcher/ui/InstanceWindow.h create mode 100644 archived/projt-launcher/launcher/ui/LaunchMenu.cpp create mode 100644 archived/projt-launcher/launcher/ui/LaunchMenu.h create mode 100644 archived/projt-launcher/launcher/ui/MainWindow.cpp create mode 100644 archived/projt-launcher/launcher/ui/MainWindow.h create mode 100644 archived/projt-launcher/launcher/ui/MainWindow.ui create mode 100644 archived/projt-launcher/launcher/ui/ViewLogWindow.cpp create mode 100644 archived/projt-launcher/launcher/ui/ViewLogWindow.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/AboutDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/AboutDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/AboutDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/BackupDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/BackupDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/BackupDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/BlockedModsDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ChooseProviderDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CopyInstanceDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CreateShortcutDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/CustomMessageBox.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportInstanceDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportPackDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ExportToModListDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/IconPickerDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ImportResourceDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/InstallLoaderDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/LauncherHubDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/MSALoginDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewComponentDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewInstanceDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewsDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewsDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/NewsDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/OfflineLoginDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProfileSelectDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProfileSetupDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ProgressDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ResourceDownloadDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ResourceUpdateDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ReviewMessageBox.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/ScrollMessageBox.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/UpdateAvailableDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/VersionSelectDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/SkinManageDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/draw/BoxGeometry.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/draw/Scene.h create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp create mode 100644 archived/projt-launcher/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h create mode 100644 archived/projt-launcher/launcher/ui/instanceview/AccessibleInstanceView.cpp create mode 100644 archived/projt-launcher/launcher/ui/instanceview/AccessibleInstanceView.h create mode 100644 archived/projt-launcher/launcher/ui/instanceview/AccessibleInstanceView_p.h create mode 100644 archived/projt-launcher/launcher/ui/instanceview/InstanceDelegate.cpp create mode 100644 archived/projt-launcher/launcher/ui/instanceview/InstanceDelegate.h create mode 100644 archived/projt-launcher/launcher/ui/instanceview/InstanceProxyModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/instanceview/InstanceProxyModel.h create mode 100644 archived/projt-launcher/launcher/ui/instanceview/InstanceView.cpp create mode 100644 archived/projt-launcher/launcher/ui/instanceview/InstanceView.h create mode 100644 archived/projt-launcher/launcher/ui/instanceview/VisualGroup.cpp create mode 100644 archived/projt-launcher/launcher/ui/instanceview/VisualGroup.h create mode 100644 archived/projt-launcher/launcher/ui/java/InstallJavaDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/java/InstallJavaDialog.h create mode 100644 archived/projt-launcher/launcher/ui/java/VersionList.cpp create mode 100644 archived/projt-launcher/launcher/ui/java/VersionList.h create mode 100644 archived/projt-launcher/launcher/ui/pagedialog/PageDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/pagedialog/PageDialog.h create mode 100644 archived/projt-launcher/launcher/ui/pages/BasePage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/BasePageContainer.h create mode 100644 archived/projt-launcher/launcher/ui/pages/BasePageProvider.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/APIPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/global/APIPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/APIPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/global/AccountListPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/global/AccountListPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/AccountListPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/global/AppearancePage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/ExternalToolsPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/global/ExternalToolsPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/ExternalToolsPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/global/JavaPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/global/JavaPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/JavaPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/global/LanguagePage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/global/LanguagePage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/LauncherPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/global/LauncherPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/LauncherPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/global/MinecraftPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/ProxyPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/global/ProxyPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/global/ProxyPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/BackupPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/BackupPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/DataPackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/DataPackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ExternalResourcesPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ExternalResourcesPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ExternalResourcesPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/InstanceSettingsPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/LogPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/LogPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/LogPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ManagedPackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ManagedPackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ManagedPackPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/McClient.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/McClient.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/McResolver.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/McResolver.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ModFolderPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ModFolderPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/NotesPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/NotesPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/NotesPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/OtherLogsPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/OtherLogsPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/OtherLogsPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ResourcePackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ResourcePackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ScreenshotsPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ScreenshotsPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ScreenshotsPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ServerPingTask.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ServerPingTask.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ServersPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ServersPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ServersPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ShaderPackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/ShaderPackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/TexturePackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/TexturePackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/VersionPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/VersionPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/VersionPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/WorldListPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/WorldListPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/instance/WorldListPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/CustomPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/CustomPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/CustomPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/DataPackModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/DataPackModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/DataPackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/DataPackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ImportPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ImportPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ImportPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ModModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ModModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ModPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ModPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ModpackProviderBasePage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/OptionalModDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/OptionalModDialog.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/OptionalModDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourceModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourceModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourcePackModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourcePackModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourcePackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourcePackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourcePage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourcePage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ResourcePage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ShaderPackModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ShaderPackModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ShaderPackPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/ShaderPackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/TexturePackModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/TexturePackModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/TexturePackPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlameModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlameModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlamePage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlamePage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlamePage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlameResourceModels.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/flame/FlameResourcePages.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/import_ftb/ListModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/legacy_ftb/Page.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/legacy_ftb/Page.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/technic/TechnicData.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/technic/TechnicModel.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/technic/TechnicModel.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/technic/TechnicPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/technic/TechnicPage.h create mode 100644 archived/projt-launcher/launcher/ui/pages/modplatform/technic/TechnicPage.ui create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/AutoJavaWizardPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/AutoJavaWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/AutoJavaWizardPage.ui create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/BaseWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/JavaWizardPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/JavaWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/LanguageWizardPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/LanguageWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/LoginWizardPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/LoginWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/LoginWizardPage.ui create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/PasteWizardPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/PasteWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/PasteWizardPage.ui create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/SearchWizardPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/SearchWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/SetupWizard.cpp create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/SetupWizard.h create mode 100644 archived/projt-launcher/launcher/ui/setupwizard/ThemeWizardPage.h create mode 100644 archived/projt-launcher/launcher/ui/tasks/LogUploadTask.cpp create mode 100644 archived/projt-launcher/launcher/ui/tasks/LogUploadTask.h create mode 100644 archived/projt-launcher/launcher/ui/themes/BrightTheme.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/BrightTheme.h create mode 100644 archived/projt-launcher/launcher/ui/themes/CatPack.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/CatPack.h create mode 100644 archived/projt-launcher/launcher/ui/themes/CatPainter.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/CatPainter.h create mode 100644 archived/projt-launcher/launcher/ui/themes/CustomTheme.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/CustomTheme.h create mode 100644 archived/projt-launcher/launcher/ui/themes/DarkTheme.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/DarkTheme.h create mode 100644 archived/projt-launcher/launcher/ui/themes/FusionTheme.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/FusionTheme.h create mode 100644 archived/projt-launcher/launcher/ui/themes/HintOverrideProxyStyle.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/HintOverrideProxyStyle.h create mode 100644 archived/projt-launcher/launcher/ui/themes/IconTheme.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/IconTheme.h create mode 100644 archived/projt-launcher/launcher/ui/themes/SystemTheme.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/SystemTheme.h create mode 100644 archived/projt-launcher/launcher/ui/themes/Theme.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/Theme.h create mode 100644 archived/projt-launcher/launcher/ui/themes/ThemeManager.cpp create mode 100644 archived/projt-launcher/launcher/ui/themes/ThemeManager.h create mode 100644 archived/projt-launcher/launcher/ui/themes/ThemeManager.mm create mode 100644 archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/AppearanceWidget.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/CefHubView.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/CefHubView.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/CheckComboBox.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/CheckComboBox.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/Common.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/Common.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/CustomCommands.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/CustomCommands.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/CustomCommands.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/EnvironmentVariables.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/FallbackHubView.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/FallbackHubView.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/FastFileIconProvider.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/FileIgnoreProxy.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/HubSearchProvider.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/HubViewBase.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/IconLabel.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/IconLabel.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/InfoFrame.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/InfoFrame.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/InfoFrame.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/JavaSettingsWidget.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/JavaWizardWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/LabeledToolButton.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/LanguageSelectionWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/LogView.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/LogView.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/MinecraftSettingsWidget.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/ModFilterWidget.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/ModListView.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/ModListView.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/PageContainer.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/PageContainer.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/PageContainer_p.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/ProgressWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/ProgressWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/ProjectDescriptionPage.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/ProjectItem.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/ProjectItem.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/QtWebEngineHubView.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/SubTaskProgressBar.ui create mode 100644 archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/VariableSizedImageObject.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/VersionListView.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/VersionListView.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/VersionSelectWidget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/WebView2Widget.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/WebView2Widget.h create mode 100644 archived/projt-launcher/launcher/ui/widgets/WideBar.cpp create mode 100644 archived/projt-launcher/launcher/ui/widgets/WideBar.h create mode 100644 archived/projt-launcher/launcher/updater/ExternalUpdater.h create mode 100644 archived/projt-launcher/launcher/updater/MacSparkleUpdater.h create mode 100644 archived/projt-launcher/launcher/updater/MacSparkleUpdater.mm create mode 100644 archived/projt-launcher/launcher/updater/ProjTExternalUpdater.cpp create mode 100644 archived/projt-launcher/launcher/updater/ProjTExternalUpdater.h create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.cpp create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/ProjTUpdater.h create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.cpp create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/ReleaseInfo.h create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/SelectReleaseDialog.ui create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.cpp create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/UpdaterDialogs.h create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/updater.exe.manifest create mode 100644 archived/projt-launcher/launcher/updater/projtupdater/updater_main.cpp create mode 100644 archived/projt-launcher/nix/maintainers.nix create mode 100644 archived/projt-launcher/nix/unwrapped.nix create mode 100644 archived/projt-launcher/nix/wrapper.nix create mode 100644 archived/projt-launcher/nuget.config create mode 100644 archived/projt-launcher/packages.config create mode 100644 archived/projt-launcher/packaging/arch/PKGBUILD.in create mode 100644 archived/projt-launcher/program_info/AdhocSignedApp.entitlements create mode 100644 archived/projt-launcher/program_info/App.entitlements create mode 100644 archived/projt-launcher/program_info/CMakeLists.txt create mode 100644 archived/projt-launcher/program_info/LICENSE.instanceicons create mode 100644 archived/projt-launcher/program_info/LICENSE.projecttick create mode 100644 archived/projt-launcher/program_info/ProjT.png create mode 100644 archived/projt-launcher/program_info/ProjTLauncher.icon/Assets/org.projecttick.ProjTLauncher_Katman 1.png create mode 100644 archived/projt-launcher/program_info/ProjTLauncher.icon/Assets/org.projecttick.ProjTLauncher_Katman 2.png create mode 100644 archived/projt-launcher/program_info/ProjTLauncher.icon/icon.json create mode 100755 archived/projt-launcher/program_info/genicons.sh create mode 100755 archived/projt-launcher/program_info/genmacicon.py create mode 100644 archived/projt-launcher/program_info/gplv3-127x51.png create mode 100644 archived/projt-launcher/program_info/instance_icons.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.Social.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.Source.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.bigsur.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.desktop.in create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.logo-darkmode.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.logo.source.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.logo.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.metainfo.xml.in create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.mime.xml create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher.svg create mode 100644 archived/projt-launcher/program_info/org.projecttick.ProjTLauncher_256.png create mode 100644 archived/projt-launcher/program_info/portable.txt create mode 100644 archived/projt-launcher/program_info/projtlauncher.6.scd.in create mode 100644 archived/projt-launcher/program_info/projtlauncher.icns create mode 100644 archived/projt-launcher/program_info/projtlauncher.ico create mode 100644 archived/projt-launcher/program_info/projtlauncher.manifest.in create mode 100644 archived/projt-launcher/program_info/projtlauncher.qrc.in create mode 100644 archived/projt-launcher/program_info/projtlauncher.rc.in create mode 100644 archived/projt-launcher/program_info/win_install.nsi.in create mode 100644 archived/projt-launcher/scripts/build-cef-from-source.sh create mode 100755 archived/projt-launcher/scripts/build-tomlplusplus.mjs create mode 100755 archived/projt-launcher/scripts/compress_images.sh create mode 100755 archived/projt-launcher/scripts/gen-cmark-config.sh create mode 100755 archived/projt-launcher/scripts/gen-cmark-export.sh create mode 100755 archived/projt-launcher/scripts/gen-cmark-version.sh create mode 100755 archived/projt-launcher/scripts/gen-nbt-export.sh create mode 100755 archived/projt-launcher/scripts/gen-qrencode-config.sh create mode 100755 archived/projt-launcher/scripts/patch_maintainer_emails.py create mode 100755 archived/projt-launcher/scripts/syncconfig.sh create mode 100755 archived/projt-launcher/scripts/update-qt-version.sh create mode 100755 archived/projt-launcher/scripts/update-subtrees.sh create mode 100644 archived/projt-launcher/shell.nix create mode 100644 archived/projt-launcher/tests/ApplicationMessage_test.cpp create mode 100644 archived/projt-launcher/tests/BaseInstanceSettings_test.cpp create mode 100644 archived/projt-launcher/tests/CMakeLists.txt create mode 100644 archived/projt-launcher/tests/CatPack_test.cpp create mode 100644 archived/projt-launcher/tests/Commandline_test.cpp create mode 100644 archived/projt-launcher/tests/DataPackParse_test.cpp create mode 100644 archived/projt-launcher/tests/DefaultVariable_test.cpp create mode 100644 archived/projt-launcher/tests/ExponentialSeries_test.cpp create mode 100644 archived/projt-launcher/tests/FileSystem_test.cpp create mode 100644 archived/projt-launcher/tests/Filter_test.cpp create mode 100644 archived/projt-launcher/tests/GZip_test.cpp create mode 100644 archived/projt-launcher/tests/GradleSpecifier_test.cpp create mode 100644 archived/projt-launcher/tests/INIFile_test.cpp create mode 100644 archived/projt-launcher/tests/Index_test.cpp create mode 100644 archived/projt-launcher/tests/InstanceCopyPrefs_test.cpp create mode 100644 archived/projt-launcher/tests/JavaVersion_test.cpp create mode 100644 archived/projt-launcher/tests/JsonHelpers_test.cpp create mode 100644 archived/projt-launcher/tests/JsonTypes_test.cpp create mode 100644 archived/projt-launcher/tests/Json_test.cpp create mode 100644 archived/projt-launcher/tests/KonamiCode_test.cpp create mode 100644 archived/projt-launcher/tests/LaunchLineRouter_test.cpp create mode 100644 archived/projt-launcher/tests/LaunchLogModel_test.cpp create mode 100644 archived/projt-launcher/tests/LaunchPipeline_test.cpp create mode 100644 archived/projt-launcher/tests/LaunchVariableExpander_test.cpp create mode 100644 archived/projt-launcher/tests/Library_test.cpp create mode 100644 archived/projt-launcher/tests/LogEventParser_test.cpp create mode 100644 archived/projt-launcher/tests/MMCTime_test.cpp create mode 100644 archived/projt-launcher/tests/MessageLevel_test.cpp create mode 100644 archived/projt-launcher/tests/MetaComponentParse_test.cpp create mode 100644 archived/projt-launcher/tests/ModPlatform_test.cpp create mode 100644 archived/projt-launcher/tests/MojangVersionFormat_test.cpp create mode 100644 archived/projt-launcher/tests/NetHeaderProxy_test.cpp create mode 100644 archived/projt-launcher/tests/NetSink_test.cpp create mode 100644 archived/projt-launcher/tests/NetUtils_test.cpp create mode 100644 archived/projt-launcher/tests/Packwiz_test.cpp create mode 100644 archived/projt-launcher/tests/ParseUtils_test.cpp create mode 100644 archived/projt-launcher/tests/ProjTExternalUpdater_test.cpp create mode 100644 archived/projt-launcher/tests/ResourceFolderModel_test.cpp create mode 100644 archived/projt-launcher/tests/ResourcePackParse_test.cpp create mode 100644 archived/projt-launcher/tests/RuntimeVersion_test.cpp create mode 100644 archived/projt-launcher/tests/SeparatorPrefixTree_test.cpp create mode 100644 archived/projt-launcher/tests/ShaderPackParse_test.cpp create mode 100644 archived/projt-launcher/tests/StringUtilsHtmlPatch_test.cpp create mode 100644 archived/projt-launcher/tests/StringUtilsNaturalCompare_test.cpp create mode 100644 archived/projt-launcher/tests/StringUtilsSplitFirst_test.cpp create mode 100644 archived/projt-launcher/tests/StringUtilsTruncateUrl_test.cpp create mode 100644 archived/projt-launcher/tests/StringUtils_test.cpp create mode 100644 archived/projt-launcher/tests/Task_test.cpp create mode 100644 archived/projt-launcher/tests/TexturePackParse_test.cpp create mode 100644 archived/projt-launcher/tests/VersionFilterData_test.cpp create mode 100644 archived/projt-launcher/tests/Version_test.cpp create mode 100644 archived/projt-launcher/tests/WorldSaveParse_test.cpp create mode 100644 archived/projt-launcher/tests/XmlLogs_test.cpp create mode 100644 archived/projt-launcher/tests/testdata/CatPacks/index.json create mode 100644 archived/projt-launcher/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta create mode 100644 archived/projt-launcher/tests/testdata/DataPackParse/test_data_pack_boogaloo.zip create mode 100644 archived/projt-launcher/tests/testdata/DataPackParse/test_folder/pack.mcmeta create mode 100755 archived/projt-launcher/tests/testdata/FileSystem/FileSystem-test_createShortcut-unix create mode 100644 archived/projt-launcher/tests/testdata/FileSystem/test_folder/.secret_folder/.secret_file.txt create mode 100644 archived/projt-launcher/tests/testdata/FileSystem/test_folder/assets/minecraft/textures/blah.txt create mode 100644 archived/projt-launcher/tests/testdata/FileSystem/test_folder/pack.mcmeta create mode 100644 archived/projt-launcher/tests/testdata/FileSystem/test_folder/pack.nfo create mode 100644 archived/projt-launcher/tests/testdata/Libraries/1.9-simple.json create mode 100644 archived/projt-launcher/tests/testdata/Libraries/1.9.json create mode 100644 archived/projt-launcher/tests/testdata/Libraries/codecwav-20101023.jar create mode 100644 archived/projt-launcher/tests/testdata/Libraries/lib-native-arch.json create mode 100644 archived/projt-launcher/tests/testdata/Libraries/lib-native.json create mode 100644 archived/projt-launcher/tests/testdata/Libraries/lib-simple.json create mode 100644 archived/projt-launcher/tests/testdata/Libraries/testname-testversion-linux-32.jar create mode 100644 archived/projt-launcher/tests/testdata/Library/1.9-simple.json create mode 100644 archived/projt-launcher/tests/testdata/Library/1.9.json create mode 100644 archived/projt-launcher/tests/testdata/Library/codecwav-20101023.jar create mode 100644 archived/projt-launcher/tests/testdata/Library/lib-native-arch.json create mode 100644 archived/projt-launcher/tests/testdata/Library/lib-native.json create mode 100644 archived/projt-launcher/tests/testdata/Library/lib-simple.json create mode 100644 archived/projt-launcher/tests/testdata/Library/testname-testversion-linux-32.jar create mode 100644 archived/projt-launcher/tests/testdata/MetaComponentParse/component_basic.json create mode 100644 archived/projt-launcher/tests/testdata/MetaComponentParse/component_with_extra.json create mode 100644 archived/projt-launcher/tests/testdata/MetaComponentParse/component_with_format.json create mode 100644 archived/projt-launcher/tests/testdata/MetaComponentParse/component_with_link.json create mode 100644 archived/projt-launcher/tests/testdata/MetaComponentParse/component_with_mixed.json create mode 100644 archived/projt-launcher/tests/testdata/PackageManifest/1.8.0_202-x64.json create mode 100755 archived/projt-launcher/tests/testdata/PackageManifest/inspect/a/b.txt create mode 120000 archived/projt-launcher/tests/testdata/PackageManifest/inspect/a/b/b.txt create mode 100644 archived/projt-launcher/tests/testdata/PackageManifest/inspect_win/a/b.txt create mode 100644 archived/projt-launcher/tests/testdata/PackageManifest/inspect_win/a/b/b.txt create mode 100644 archived/projt-launcher/tests/testdata/Packwiz/borderless-mining.pw.toml create mode 100644 archived/projt-launcher/tests/testdata/Packwiz/screenshot-to-clipboard-fabric.pw.toml create mode 100644 archived/projt-launcher/tests/testdata/ResourceFolderModel/another_test_folder/pack.mcmeta create mode 100644 archived/projt-launcher/tests/testdata/ResourceFolderModel/supercoolmod.jar create mode 100644 archived/projt-launcher/tests/testdata/ResourceFolderModel/test_folder/assets/minecraft/textures/blah.txt create mode 100644 archived/projt-launcher/tests/testdata/ResourceFolderModel/test_folder/pack.mcmeta create mode 100644 archived/projt-launcher/tests/testdata/ResourceFolderModel/test_folder/pack.nfo create mode 100644 archived/projt-launcher/tests/testdata/ResourceFolderModel/test_resource_pack_idk.zip create mode 100644 archived/projt-launcher/tests/testdata/Resources/another_test_folder/pack.mcmeta create mode 100644 archived/projt-launcher/tests/testdata/Resources/supercoolmod.jar create mode 100644 archived/projt-launcher/tests/testdata/Resources/test_folder/assets/minecraft/textures/blah.txt create mode 100644 archived/projt-launcher/tests/testdata/Resources/test_folder/pack.mcmeta create mode 100644 archived/projt-launcher/tests/testdata/Resources/test_folder/pack.nfo create mode 100644 archived/projt-launcher/tests/testdata/Resources/test_resource_pack_idk.zip create mode 100644 archived/projt-launcher/tests/testdata/ShaderPackParse/shaderpack1.zip create mode 100644 archived/projt-launcher/tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties create mode 100644 archived/projt-launcher/tests/testdata/ShaderPackParse/shaderpack3.zip create mode 100644 archived/projt-launcher/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt create mode 100644 archived/projt-launcher/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt create mode 100644 archived/projt-launcher/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt create mode 100644 archived/projt-launcher/tests/testdata/TexturePackParse/another_test_texturefolder/pack.txt create mode 100644 archived/projt-launcher/tests/testdata/TexturePackParse/test_texture_pack_idk.zip create mode 100644 archived/projt-launcher/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/textures/blah.txt create mode 100644 archived/projt-launcher/tests/testdata/TexturePackParse/test_texturefolder/pack.txt create mode 100644 archived/projt-launcher/tests/testdata/Version/test_vectors.txt create mode 100644 archived/projt-launcher/tests/testdata/WorldSaveParse/minecraft_save_1.zip create mode 100644 archived/projt-launcher/tests/testdata/WorldSaveParse/minecraft_save_2.zip create mode 100644 archived/projt-launcher/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat create mode 100644 archived/projt-launcher/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat create mode 100755 archived/projt-launcher/tools/generate_todo_report.py create mode 100644 archived/projt-launcher/tools/get_commits.py create mode 100644 archived/projt-minicraft-modpack/LICENSE create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/MiniCraft S1 1.10.2-1.10.2 10.0 Alpha.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/MiniCraft S1 Beta-13.0 13w990a.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/MiniCraft S1-12.7.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/MiniCraft S1-13.0.0.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/MiniCraft S1-13.0.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/MiniCraft S1-Pre-release 2.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/minicraft Beta -12.7 12w957g.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/minicraft-12.1.5.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/minicraft-12.3.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/minicraft-12.5.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S1/minicraft-12.6.2.9.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S2/MiniCraft S2-A00051c74C.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S2/MiniCraft S2-L3.0.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S2/MiniCraft S2-R10056a75A.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S2/MiniCraft S2-R10171a76A.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S2/Minicraft S2-N1.0.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S2/Minicraft S3-N2.0.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3 DEV-1.2.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.0.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.0.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.1.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.2.0.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.2.0.2.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.2.0.3.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S3/MiniCraft S3-1.2.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4 ALPHA-0.0.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4 ALPHA-0.0.2.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4 ALPHA-0.0.3.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4 BETA-0.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4 BETA-0.2.1.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4 BETA-0.2.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4-1.0.0.zip create mode 100644 archived/projt-minicraft-modpack/MiniCraft/MiniCraft S4/MiniCraft S4-LASTMAJORRELEASE-2.0.0.zip create mode 100644 archived/projt-minicraft-modpack/README.md create mode 100644 archived/projt-modpack/.DS_Store create mode 100644 archived/projt-modpack/.gitattributes create mode 100644 archived/projt-modpack/COPYING.md create mode 100644 archived/projt-modpack/LICENSE create mode 100644 archived/projt-modpack/ProjT1.png create mode 100644 archived/projt-modpack/ProjT2.png create mode 100644 archived/projt-modpack/ProjT3.png create mode 100644 archived/projt-modpack/README.md create mode 100644 archived/projt-modpack/affiliate-banner-bg.webp create mode 100644 archived/projt-modpack/affiliate-banner-fg.webp create mode 100644 archived/projt-modpack/bisect-icon.webp create mode 100644 archived/ptlibzippy/.cmake-format.yaml create mode 100644 archived/ptlibzippy/.gitignore create mode 100644 archived/ptlibzippy/BUILD.bazel create mode 100644 archived/ptlibzippy/CMakeLists.txt create mode 100644 archived/ptlibzippy/COPYING.md create mode 100644 archived/ptlibzippy/FAQ create mode 100644 archived/ptlibzippy/INDEX create mode 100644 archived/ptlibzippy/MODULE.bazel create mode 100644 archived/ptlibzippy/Makefile.in create mode 100644 archived/ptlibzippy/README create mode 100644 archived/ptlibzippy/README-cmake.md create mode 100644 archived/ptlibzippy/adler32.c create mode 100644 archived/ptlibzippy/amiga/Makefile.pup create mode 100644 archived/ptlibzippy/amiga/Makefile.sas create mode 100644 archived/ptlibzippy/compress.c create mode 100755 archived/ptlibzippy/configure create mode 100644 archived/ptlibzippy/contrib/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/README.contrib create mode 100644 archived/ptlibzippy/contrib/ada/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/ada/buffer_demo.adb create mode 100644 archived/ptlibzippy/contrib/ada/cmake/Modules/CMakeADACompiler.cmake.in create mode 100644 archived/ptlibzippy/contrib/ada/cmake/Modules/CMakeADAInformation.cmake create mode 100644 archived/ptlibzippy/contrib/ada/cmake/Modules/CMakeDetermineADACompiler.cmake create mode 100644 archived/ptlibzippy/contrib/ada/cmake/Modules/CMakeTestADACompiler.cmake create mode 100644 archived/ptlibzippy/contrib/ada/cmake/binder_helper.cmake create mode 100644 archived/ptlibzippy/contrib/ada/cmake/compile_helper.cmake create mode 100644 archived/ptlibzippy/contrib/ada/cmake/exe_link_helper.cmake create mode 100644 archived/ptlibzippy/contrib/ada/cmake/shared_link_helper.cmake create mode 100644 archived/ptlibzippy/contrib/ada/cmake/static_link_helper.cmake create mode 100644 archived/ptlibzippy/contrib/ada/mtest.adb create mode 100644 archived/ptlibzippy/contrib/ada/ptlib-streams.adb create mode 100644 archived/ptlibzippy/contrib/ada/ptlib-streams.ads create mode 100644 archived/ptlibzippy/contrib/ada/ptlib-thin.adb create mode 100644 archived/ptlibzippy/contrib/ada/ptlib-thin.ads create mode 100644 archived/ptlibzippy/contrib/ada/ptlib.adb create mode 100644 archived/ptlibzippy/contrib/ada/ptlib.ads create mode 100644 archived/ptlibzippy/contrib/ada/read.adb create mode 100644 archived/ptlibzippy/contrib/ada/readme.txt create mode 100644 archived/ptlibzippy/contrib/ada/test.adb create mode 100644 archived/ptlibzippy/contrib/ada/zlib.gpr create mode 100644 archived/ptlibzippy/contrib/blast/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/blast/README create mode 100644 archived/ptlibzippy/contrib/blast/blast-test.c create mode 100644 archived/ptlibzippy/contrib/blast/blast.c create mode 100644 archived/ptlibzippy/contrib/blast/blast.h create mode 100644 archived/ptlibzippy/contrib/blast/blastConfig.cmake.in create mode 100644 archived/ptlibzippy/contrib/blast/test.pk create mode 100644 archived/ptlibzippy/contrib/blast/test.txt create mode 100644 archived/ptlibzippy/contrib/blast/test/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/blast/test/add_subdirectory_exclude_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/blast/test/add_subdirectory_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/blast/test/find_package_no_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/blast/test/find_package_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/blast/test/find_package_wrong_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/blast/tester.cmake create mode 100644 archived/ptlibzippy/contrib/crc32vx/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/crc32vx/README create mode 100644 archived/ptlibzippy/contrib/crc32vx/crc32_vx.c create mode 100644 archived/ptlibzippy/contrib/crc32vx/crc32_vx_hooks.h create mode 100644 archived/ptlibzippy/contrib/delphi/ZLib.pas create mode 100644 archived/ptlibzippy/contrib/delphi/ZLibConst.pas create mode 100644 archived/ptlibzippy/contrib/delphi/readme.txt create mode 100644 archived/ptlibzippy/contrib/delphi/zlibd32.mak create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib.build create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib.chm create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib.sln create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/AssemblyInfo.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/ChecksumImpl.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/CircularBuffer.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/CodecBase.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/Deflater.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/DotZLib.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/DotZLib.csproj create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/GZipStream.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/Inflater.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/DotZLib/UnitTests.cs create mode 100644 archived/ptlibzippy/contrib/dotzlib/LICENSE_1_0.txt create mode 100644 archived/ptlibzippy/contrib/dotzlib/readme.txt create mode 100644 archived/ptlibzippy/contrib/gcc_gvmat64/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/gcc_gvmat64/gvmat64.S create mode 100644 archived/ptlibzippy/contrib/infback9/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/infback9/README create mode 100644 archived/ptlibzippy/contrib/infback9/infback9.c create mode 100644 archived/ptlibzippy/contrib/infback9/infback9.h create mode 100644 archived/ptlibzippy/contrib/infback9/inffix9.h create mode 100644 archived/ptlibzippy/contrib/infback9/inflate9.h create mode 100644 archived/ptlibzippy/contrib/infback9/inftree9.c create mode 100644 archived/ptlibzippy/contrib/infback9/inftree9.h create mode 100644 archived/ptlibzippy/contrib/iostream/test.cpp create mode 100644 archived/ptlibzippy/contrib/iostream/zfstream.cpp create mode 100644 archived/ptlibzippy/contrib/iostream/zfstream.h create mode 100644 archived/ptlibzippy/contrib/iostream2/zstream.h create mode 100644 archived/ptlibzippy/contrib/iostream2/zstream_test.cpp create mode 100644 archived/ptlibzippy/contrib/iostream3/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/iostream3/README create mode 100644 archived/ptlibzippy/contrib/iostream3/TODO create mode 100644 archived/ptlibzippy/contrib/iostream3/iostream3Config.cmake.in create mode 100644 archived/ptlibzippy/contrib/iostream3/test.cc create mode 100644 archived/ptlibzippy/contrib/iostream3/test/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/iostream3/test/add_subdirectory_exclude_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/iostream3/test/add_subdirectory_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/iostream3/test/find_package_no_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/iostream3/test/find_package_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/iostream3/test/find_package_wrong_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/iostream3/zfstream.cc create mode 100644 archived/ptlibzippy/contrib/iostream3/zfstream.h create mode 100644 archived/ptlibzippy/contrib/minizip/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/minizip/LICENSE.Info-Zip create mode 100644 archived/ptlibzippy/contrib/minizip/Makefile.am create mode 100644 archived/ptlibzippy/contrib/minizip/MiniZip64_Changes.txt create mode 100644 archived/ptlibzippy/contrib/minizip/MiniZip64_info.txt create mode 100644 archived/ptlibzippy/contrib/minizip/configure.ac create mode 100644 archived/ptlibzippy/contrib/minizip/crypt.h create mode 100644 archived/ptlibzippy/contrib/minizip/ints.h create mode 100644 archived/ptlibzippy/contrib/minizip/ioapi.c create mode 100644 archived/ptlibzippy/contrib/minizip/ioapi.h create mode 100644 archived/ptlibzippy/contrib/minizip/iowin32.c create mode 100644 archived/ptlibzippy/contrib/minizip/iowin32.h create mode 100644 archived/ptlibzippy/contrib/minizip/make_vms.com create mode 100644 archived/ptlibzippy/contrib/minizip/miniunz.c create mode 100644 archived/ptlibzippy/contrib/minizip/miniunzip.1 create mode 100644 archived/ptlibzippy/contrib/minizip/minizip.1 create mode 100644 archived/ptlibzippy/contrib/minizip/minizip.c create mode 100644 archived/ptlibzippy/contrib/minizip/minizip.pc.in create mode 100644 archived/ptlibzippy/contrib/minizip/minizip.pc.txt create mode 100644 archived/ptlibzippy/contrib/minizip/minizipConfig.cmake.in create mode 100644 archived/ptlibzippy/contrib/minizip/mztools.c create mode 100644 archived/ptlibzippy/contrib/minizip/mztools.h create mode 100644 archived/ptlibzippy/contrib/minizip/skipset.h create mode 100644 archived/ptlibzippy/contrib/minizip/test/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/minizip/test/add_subdirectory_exclude_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/minizip/test/add_subdirectory_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/minizip/test/find_package_no_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/minizip/test/find_package_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/minizip/test/find_package_wrong_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/minizip/test/test_helper.cm create mode 100644 archived/ptlibzippy/contrib/minizip/unzip.c create mode 100644 archived/ptlibzippy/contrib/minizip/unzip.h create mode 100644 archived/ptlibzippy/contrib/minizip/zip.c create mode 100644 archived/ptlibzippy/contrib/minizip/zip.h create mode 100644 archived/ptlibzippy/contrib/nuget/nuget.csproj create mode 100644 archived/ptlibzippy/contrib/nuget/nuget.sln create mode 100644 archived/ptlibzippy/contrib/pascal/example.pas create mode 100644 archived/ptlibzippy/contrib/pascal/readme.txt create mode 100644 archived/ptlibzippy/contrib/pascal/zlibd32.mak create mode 100644 archived/ptlibzippy/contrib/pascal/zlibpas.pas create mode 100644 archived/ptlibzippy/contrib/puff/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/puff/README create mode 100644 archived/ptlibzippy/contrib/puff/bin-writer.c create mode 100644 archived/ptlibzippy/contrib/puff/puff.c create mode 100644 archived/ptlibzippy/contrib/puff/puff.h create mode 100644 archived/ptlibzippy/contrib/puff/puffConfig.cmake.in create mode 100644 archived/ptlibzippy/contrib/puff/pufftest.c create mode 100644 archived/ptlibzippy/contrib/puff/test/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/puff/test/add_subdirectory_exclude_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/puff/test/add_subdirectory_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/puff/test/find_package_no_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/puff/test/find_package_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/puff/test/find_package_wrong_components_test.cmake.in create mode 100644 archived/ptlibzippy/contrib/puff/test/tester-cov.cmake create mode 100644 archived/ptlibzippy/contrib/puff/test/tester.cmake create mode 100644 archived/ptlibzippy/contrib/puff/zeros.raw create mode 100644 archived/ptlibzippy/contrib/testptlibzippy/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/testptlibzippy/testptlibzippy.c create mode 100644 archived/ptlibzippy/contrib/testptlibzippy/testptlibzippy.txt create mode 100644 archived/ptlibzippy/contrib/vstudio/readme.txt create mode 100644 archived/ptlibzippy/contrib/zlib1-dll/CMakeLists.txt create mode 100644 archived/ptlibzippy/contrib/zlib1-dll/readme.txt create mode 100644 archived/ptlibzippy/crc32.c create mode 100644 archived/ptlibzippy/crc32.h create mode 100644 archived/ptlibzippy/deflate.c create mode 100644 archived/ptlibzippy/deflate.h create mode 100644 archived/ptlibzippy/doc/algorithm.txt create mode 100644 archived/ptlibzippy/doc/crc-doc.1.0.pdf create mode 100644 archived/ptlibzippy/doc/rfc1950.txt create mode 100644 archived/ptlibzippy/doc/rfc1951.txt create mode 100644 archived/ptlibzippy/doc/rfc1952.txt create mode 100644 archived/ptlibzippy/doc/txtvsbin.txt create mode 100644 archived/ptlibzippy/examples/README.examples create mode 100644 archived/ptlibzippy/examples/enough.c create mode 100644 archived/ptlibzippy/examples/fitblk.c create mode 100644 archived/ptlibzippy/examples/gun.c create mode 100644 archived/ptlibzippy/examples/gzappend.c create mode 100644 archived/ptlibzippy/examples/gzjoin.c create mode 100644 archived/ptlibzippy/examples/gzlog.c create mode 100644 archived/ptlibzippy/examples/gzlog.h create mode 100644 archived/ptlibzippy/examples/gznorm.c create mode 100644 archived/ptlibzippy/examples/zlib_how.html create mode 100644 archived/ptlibzippy/examples/zpipe.c create mode 100644 archived/ptlibzippy/examples/zran.c create mode 100644 archived/ptlibzippy/examples/zran.h create mode 100644 archived/ptlibzippy/gzclose.c create mode 100644 archived/ptlibzippy/gzlib.c create mode 100644 archived/ptlibzippy/gzread.c create mode 100644 archived/ptlibzippy/gzwrite.c create mode 100644 archived/ptlibzippy/infback.c create mode 100644 archived/ptlibzippy/inffast.c create mode 100644 archived/ptlibzippy/inffast.h create mode 100644 archived/ptlibzippy/inffixed.h create mode 100644 archived/ptlibzippy/inflate.c create mode 100644 archived/ptlibzippy/inflate.h create mode 100644 archived/ptlibzippy/inftrees.c create mode 100644 archived/ptlibzippy/inftrees.h create mode 100644 archived/ptlibzippy/make_vms.com create mode 100644 archived/ptlibzippy/msdos/Makefile.bor create mode 100644 archived/ptlibzippy/msdos/Makefile.dj2 create mode 100644 archived/ptlibzippy/msdos/Makefile.emx create mode 100644 archived/ptlibzippy/msdos/Makefile.msc create mode 100644 archived/ptlibzippy/msdos/Makefile.tc create mode 100644 archived/ptlibzippy/os400/README400 create mode 100644 archived/ptlibzippy/os400/bndsrc create mode 100644 archived/ptlibzippy/os400/make.sh create mode 100644 archived/ptlibzippy/os400/ptlibzippyfixed.rpgle create mode 100644 archived/ptlibzippy/os400/ptlibzippyfree.rpgle create mode 100644 archived/ptlibzippy/ptlibzippy.3 create mode 100644 archived/ptlibzippy/ptlibzippy.3.pdf create mode 100644 archived/ptlibzippy/ptlibzippy.h create mode 100644 archived/ptlibzippy/ptlibzippy.map create mode 100644 archived/ptlibzippy/ptlibzippy.pc.cmakein create mode 100644 archived/ptlibzippy/ptlibzippy.pc.in create mode 100644 archived/ptlibzippy/ptlibzippyConfig.cmake.in create mode 100644 archived/ptlibzippy/ptlibzippy_pngshim.c create mode 100644 archived/ptlibzippy/ptzippyconf.h.in create mode 100644 archived/ptlibzippy/ptzippyguts.h create mode 100644 archived/ptlibzippy/ptzippyutil.c create mode 100644 archived/ptlibzippy/ptzippyutil.h create mode 100644 archived/ptlibzippy/qnx/package.qpg create mode 100644 archived/ptlibzippy/test/CMakeLists.txt create mode 100644 archived/ptlibzippy/test/add_subdirectory_exclude_test.cmake.in create mode 100644 archived/ptlibzippy/test/add_subdirectory_test.cmake.in create mode 100644 archived/ptlibzippy/test/example.c create mode 100644 archived/ptlibzippy/test/find_package_no_components_test.cmake.in create mode 100644 archived/ptlibzippy/test/find_package_test.cmake.in create mode 100644 archived/ptlibzippy/test/find_package_wrong_components_test.cmake.in create mode 100644 archived/ptlibzippy/test/infcover.c create mode 100644 archived/ptlibzippy/test/minigzip.c create mode 100644 archived/ptlibzippy/treebuild.xml create mode 100644 archived/ptlibzippy/trees.c create mode 100644 archived/ptlibzippy/trees.h create mode 100644 archived/ptlibzippy/uncompr.c create mode 100644 archived/ptlibzippy/watcom/watcom_f.mak create mode 100644 archived/ptlibzippy/watcom/watcom_l.mak create mode 100644 archived/ptlibzippy/win32/DLL_FAQ.txt create mode 100644 archived/ptlibzippy/win32/Makefile.bor create mode 100644 archived/ptlibzippy/win32/Makefile.gcc create mode 100644 archived/ptlibzippy/win32/Makefile.msc create mode 100644 archived/ptlibzippy/win32/README-WIN32.txt create mode 100644 archived/ptlibzippy/win32/VisualC.txt create mode 100644 archived/ptlibzippy/win32/zlib.def create mode 100644 archived/ptlibzippy/win32/zlib1.rc (limited to 'archived') diff --git a/archived/projt-launcher/.clang-format b/archived/projt-launcher/.clang-format new file mode 100644 index 0000000000..fc3e84cca4 --- /dev/null +++ b/archived/projt-launcher/.clang-format @@ -0,0 +1,230 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: Consecutive +AlignConsecutiveAssignments: Consecutive +AlignConsecutiveBitFields: Consecutive +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Right +AlignOperands: AlignAfterOperator +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortEnumsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +AttributeMacros: + - TOML_ABSTRACT_INTERFACE + - TOML_CALLCONV + - TOML_CLOSED_ENUM + - TOML_CLOSED_FLAGS_ENUM + - TOML_EMPTY_BASES + - TOML_EXPORTED_CLASS + - TOML_FLAGS_ENUM + - TOML_OPEN_ENUM + - TOML_OPEN_FLAGS_ENUM + - TOML_TRIVIAL_ABI + - TOML_UNLIKELY_CASE +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^([/*!#]|\s*(===|---|clang-format))' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DeriveLineEnding: false +DerivePointerAlignment: false +DisableFormat: false +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +StatementAttributeLikeMacros: + - Q_EMIT +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: Indent +IndentRequires: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 1 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 1000000 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Left +ReflowComments: true +SortIncludes: false +SortJavaStaticImport: Before +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementMacros: + - __pragma + - _Pragma + - TOML_ALWAYS_INLINE + - TOML_API + - TOML_ATTR + - TOML_CONST_GETTER + - TOML_CONST_INLINE_GETTER + - TOML_EXPORTED_CLASS + - TOML_EXPORTED_MEMBER_FUNCTION + - TOML_EXPORTED_FREE_FUNCTION + - TOML_EXPORTED_STATIC_FUNCTION + - TOML_EXTERN + - TOML_EXTERNAL_LINKAGE + - TOML_INTERNAL_LINKAGE + - TOML_LIKELY_CASE + - TOML_MEMBER_ATTR + - TOML_NEVER_INLINE + - TOML_NODISCARD + - TOML_NODISCARD_CTOR + - TOML_PRAGMA_CLANG + - TOML_PRAGMA_CLANG_GE_9 + - TOML_PRAGMA_CLANG_GE_10 + - TOML_PRAGMA_CLANG_GE_11 + - TOML_PRAGMA_GCC + - TOML_PRAGMA_MSVC + - TOML_PRAGMA_ICC + - TOML_PURE_GETTER + - TOML_PURE_INLINE_GETTER + - TOML_RETURNS_BY_THROWING +TabWidth: 4 +TypenameMacros: +UseCRLF: false +UseTab: Always +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - TOML_ATTR + - TOML_CONCAT + - TOML_HAS_INCLUDE + - TOML_LIKELY + - TOML_MAKE_STRING + - TOML_PRAGMA_CLANG + - TOML_PRAGMA_CLANG_GE_9 + - TOML_PRAGMA_CLANG_GE_10 + - TOML_PRAGMA_CLANG_GE_11 + - TOML_PRAGMA_GCC + - TOML_PRAGMA_MSVC + - TOML_PRAGMA_ICC + - TOML_UNLIKELY +... +--- +Language: ObjC +BasedOnStyle: Chromium +IndentWidth: 4 +UseTab: Never +ColumnLimit: 120 +SortIncludes: false +--- +Language: JavaScript +BasedOnStyle: Chromium +IndentWidth: 4 +ColumnLimit: 140 +JavaScriptQuotes: Double +SortIncludes: false +--- +Language: Json +DisableFormat: true diff --git a/archived/projt-launcher/.clang-format-ignore b/archived/projt-launcher/.clang-format-ignore new file mode 100644 index 0000000000..94bdf609f1 --- /dev/null +++ b/archived/projt-launcher/.clang-format-ignore @@ -0,0 +1 @@ +tomlplusplus/toml.hpp \ No newline at end of file diff --git a/archived/projt-launcher/.clang-tidy b/archived/projt-launcher/.clang-tidy new file mode 100644 index 0000000000..fde2d7e609 --- /dev/null +++ b/archived/projt-launcher/.clang-tidy @@ -0,0 +1,48 @@ +Checks: > + # Enable core static analysis checks + clang-analyzer-*, + + # Best practices and maintainability + cppcoreguidelines-*, + modernize-*, + -modernize-use-trailing-return-type, + performance-*, + readability-*, + + # Catch potential bugs + bugprone-*, + misc-*, + + # Ensure safer memory management + cert-*, + + # Security checks + security-*, + + # Avoid shadowing issues + clang-analyzer-core.Shadowing, + cppcoreguidelines-avoid-variable-shadowing + +# This file mirrors quazip/.clang-tidy and is placed at repository root.Checks: + - modernize-use-using + - readability-avoid-const-params-in-decls + - misc-unused-parameters, + - readability-identifier-naming + +# ^ Without unused-parameters the readability-identifier-naming check doesn't cause any warnings. + +CheckOptions: + - { key: readability-identifier-naming.ClassCase, value: PascalCase } + - { key: readability-identifier-naming.EnumCase, value: PascalCase } + - { key: readability-identifier-naming.FunctionCase, value: camelCase } + - { key: readability-identifier-naming.GlobalVariableCase, value: camelCase } + - { key: readability-identifier-naming.GlobalFunctionCase, value: camelCase } + - { key: readability-identifier-naming.GlobalConstantCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.MacroDefinitionCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.ClassMemberCase, value: camelCase } + - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } + - { key: readability-identifier-naming.ProtectedMemberPrefix, value: m_ } + - { key: readability-identifier-naming.PrivateStaticMemberPrefix, value: s_ } + - { key: readability-identifier-naming.ProtectedStaticMemberPrefix, value: s_ } + - { key: readability-identifier-naming.PublicStaticConstantCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.EnumConstantCase, value: SCREAMING_SNAKE_CASE } \ No newline at end of file diff --git a/archived/projt-launcher/.clusterfuzzlite/Dockerfile b/archived/projt-launcher/.clusterfuzzlite/Dockerfile new file mode 100644 index 0000000000..dfd00ecc26 --- /dev/null +++ b/archived/projt-launcher/.clusterfuzzlite/Dockerfile @@ -0,0 +1,15 @@ +FROM gcr.io/oss-fuzz-base/base-builder + +# Minimal dependencies for fuzzer build +# Qt6 will be installed via aqt in build.sh +RUN apt-get update && apt-get install -y --no-install-recommends \ + ninja-build \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# Copy the source into the image +WORKDIR /src/projt-launcher +COPY . /src/projt-launcher + +# Ensure the build script is available at the root for the builder entrypoint +COPY .clusterfuzzlite/build.sh /src/build.sh diff --git a/archived/projt-launcher/.clusterfuzzlite/build.sh b/archived/projt-launcher/.clusterfuzzlite/build.sh new file mode 100644 index 0000000000..1ef4371a23 --- /dev/null +++ b/archived/projt-launcher/.clusterfuzzlite/build.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# In the OSS-Fuzz builder, the repo lives at /src/projtlauncher; locally this script sits in .clusterfuzzlite/ +if [ -d "${SCRIPT_DIR}/projtlauncher" ]; then + REPO_ROOT="$(cd "${SCRIPT_DIR}/projtlauncher" && pwd)" +elif [ -f "${SCRIPT_DIR}/CMakeLists.txt" ]; then + REPO_ROOT="${SCRIPT_DIR}" +else + REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +fi +cd "${REPO_ROOT}" + +if command -v apt-get >/dev/null 2>&1; then + apt-get update + apt-get install -y --no-install-recommends \ + ninja-build \ + python3-pip +fi + +# Qt6 not available in OSS-Fuzz environment +# Only fuzz_nbt_reader is built (uses libnbt++ and zlib, no Qt dependency) +# fuzz_gzip requires Qt6::Core (for QByteArray/QFile in GZip.cpp) - skipped + +export PATH="${PATH}" + +# Configure with fuzzing flags +cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DBUILD_FUZZERS=ON \ + -DLAUNCHER_FUZZ_ONLY=ON \ + -DCMAKE_C_COMPILER="${CC:-clang}" \ + -DCMAKE_CXX_COMPILER="${CXX:-clang++}" \ + -DCMAKE_BUILD_RPATH="\$ORIGIN" \ + -DCMAKE_INSTALL_RPATH="\$ORIGIN" \ + -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON + +# Build only fuzz_nbt_reader (no Qt dependency) +# fuzz_gzip is conditionally built only when Qt6::Core is available +cmake --build build --parallel --target fuzz_nbt_reader + +# Copy fuzzers to output directory +cp build/fuzz_nbt_reader "$OUT/" \ No newline at end of file diff --git a/archived/projt-launcher/.editorconfig b/archived/projt-launcher/.editorconfig new file mode 100644 index 0000000000..6bdd817c98 --- /dev/null +++ b/archived/projt-launcher/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[quazip/**] +end_of_line = lf +indent_style = space +indent_size = 4 + +[quazip/**/*.yml] +indent_size = 2 + +[quazip/**/*.yaml] +indent_size = 2 +# EditorConfig specs and documentation: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# C++ Code Style settings +[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] +cpp_generate_documentation_comments = doxygen_slash_star diff --git a/archived/projt-launcher/.envrc b/archived/projt-launcher/.envrc new file mode 100644 index 0000000000..a5d4d889a5 --- /dev/null +++ b/archived/projt-launcher/.envrc @@ -0,0 +1,2 @@ +LC_ALL=C use nix +LC_ALL=C watch_file nix/*.nix diff --git a/archived/projt-launcher/.gitattributes b/archived/projt-launcher/.gitattributes new file mode 100644 index 0000000000..1ef6752900 --- /dev/null +++ b/archived/projt-launcher/.gitattributes @@ -0,0 +1,89 @@ +# ================================================= +# ProjT Launcher - Global defaults +# ================================================= +* text=auto eol=lf + +# ================================================= +# Binary / never-touch files +# ================================================= +*.ref binary +*.pem binary + +*.doc binary +*.docx binary +*.pdf binary +*.ai binary +*.bin binary +*.bmp binary +*.dat binary +*.gif binary +*.ico binary +*.jpeg binary +*.jpg binary +*.otf binary +*.png binary +*.psd binary +*.ttf binary +*.woff binary +*.woff2 binary +*.xlsx binary + +# ================================================= +# Git export hygiene +# ================================================= +.gitattributes export-ignore +.gitignore export-ignore +.github/** export-ignore + +# ================================================= +# Test data must remain byte-exact +# ================================================= +**/testdata/** -text -diff + +# ================================================= +# quazip subproject (upstream-accurate behavior) +# ================================================= +# Old Git compatibility (do not normalize) +quazip/*.h -crlf +quazip/*.cpp -crlf +quazip/*.c -crlf +quazip/*.dox -crlf +quazip/CMakeLists.txt -crlf +quazip/*.in -crlf +quazip/*.cmakein -crlf +quazip/*.symbols -crlf +quazip/.editorconfig -crlf + +# Modern Git behavior +quazip/*.h text eol=lf +quazip/*.cpp text eol=lf +quazip/*.c text eol=lf +quazip/*.dox text eol=lf +quazip/CMakeLists.txt text eol=lf +quazip/*.in text eol=lf +quazip/*.cmakein text eol=lf +quazip/*.symbols text eol=lf +quazip/.editorconfig eol=lf + +# ================================================= +# tomlplusplus (Windows toolchain aware) +# ================================================= +# Visual Studio / Windows-native files +tomlplusplus/*.sln text eol=crlf encoding=UTF-8-BOM +tomlplusplus/*.vcxproj text eol=crlf encoding=UTF-8-BOM +tomlplusplus/*.vcxproj.filters text eol=crlf encoding=UTF-8-BOM +tomlplusplus/*.rc text eol=crlf encoding=UTF-8 +tomlplusplus/*.hlsl text eol=crlf encoding=UTF-8 + +# Language-specific diffs +tomlplusplus/*.cs text eol=lf diff=csharp +tomlplusplus/*.dot diff=astextplain +tomlplusplus/*.DOT diff=astextplain +tomlplusplus/*.rtf diff=astextplain +tomlplusplus/*.RTF diff=astextplain + +# ================================================= +# Vendor / third-party code +# ================================================= +vendor/** linguist-vendored +tomlplusplus/vendor/** linguist-vendored diff --git a/archived/projt-launcher/.gitignore b/archived/projt-launcher/.gitignore new file mode 100644 index 0000000000..42ba746f6c --- /dev/null +++ b/archived/projt-launcher/.gitignore @@ -0,0 +1,553 @@ +# bzip2 subproject ignores (root-relative) +bzip2/CMakeCache.txt +bzip2/CMakeFiles/ +bzip2/cmake_install.cmake +bzip2/install_manifest.txt +bzip2/CTestTestfile.cmake +bzip2/build.ninja +bzip2/rules.ninja +bzip2/.ninja_deps +bzip2/.ninja_log +bzip2/lib*.dylib +bzip2/lib*.so +bzip2/lib*.so.* +bzip2/lib*.a +docs/TODO_FIXME_REPORT.md +docs/TODO.md +# example build directories +bzip2/build +bzip2/builddir + +# vscode config directory +bzip2/.vscode + +# Windows build outputs +bzip2/*.dll +bzip2/*.exe +bzip2/*.exp +bzip2/*.lib +bzip2/*.obj +bzip2/*.res + +# python unit tests +bzip2/__pycache__ + +# quazip subproject ignores (root-relative) +quazip/doc/ +quazip/build/ +quazip/build_release/ +quazip/build_debug/ +quazip/lib/ +quazip/*.tags +quazip/*.user +quazip/*.swp +quazip/.idea/ +quazip/cmake-* +quazip/CMakeUserPresets.json +quazip/vcpkg_installed/ +quazip/Testing/ +quazip/minizip/ +quazip/CMakeFiles/ +quazip/CMakeCache.txt + +# Keep repository-level ignores intact (don't duplicate other project ignores)Thumbs.db +*.kdev4 +.user +.directory +resources/CMakeFiles +*~ +*.swp +html/ + +# Project Files +*.pro.user +CMakeLists.txt.user +CMakeLists.txt.user.* +CMakeSettings.json +/CMakeFiles +CMakeCache.txt +CMakeUserPresets.json +/.project +/.settings +/.idea +/.vscode +/.vs +cmake-build-*/ +Debug +compile_commands.json +_site/ +# Build dirs +build +/build-* + +# Ctags File +tags + +# YouCompleteMe config stuff. +.ycm_extra_conf.* + +#OSX Stuff +.DS_Store + +branding/ +secrets/ + +.cache/ +.pre-commit-config.yaml +# Nix/NixOS +.direnv/ +## Used when manually invoking stdenv phases +outputs/ +## Regular artifacts +result +result-* +repl-result-* + +# Flatpak +.flatpak-builder +flatbuild + +# Snap +*.snap + +# News +vcpkg_installed/ +obj-*/ +*.log + + +tree.txt + +node_modules/ +.qtcreator/ +.wiki-local/ +__pycache__/ +.venv/ + +zlib/*.diff +zlib/*.patch +zlib/*.orig +zlib/*.rej + +zlib/*~ +zlib/*.a +zlib/*.lo +zlib/*.o +zlib/*.dylib +zlib/*.gcda +zlib/*.gcno +zlib/*.gcov + +zlib/example +zlib/example64 +zlib/examplesh +**zlib/libz.so* +zlib/minigzip +zlib/minigzip64 +zlib/minigzipsh +zlib.pc +zlib/configure.log +zlib/build + +zlib/.DS_Store +zlib/.vs +zlib/*.user +zlib/*.nupkg +zlib/contrib/vstudio/vc143/x86 +zlib/contrib/vstudio/vc143/x64 +zlib/contrib/vstudio/vc143/arm +zlib/contrib/vstudio/vc143/arm64 +zlib/contrib/nuget/bin +zlib/contrib/nuget/obj +zlib/*.included + +# Bazel directories +zlib/bazel-* +zlib/bazel-bin +zlib/bazel-genfiles +zlib/bazel-out +zlib/bazel-testlogs +zlib/user.bazelrc + +# MODULE.bazel.lock is ignored for now as per this recommendation: +# https://github.com/bazelbuild/bazel/issues/20369 +zlib/MODULE.bazel.lock + +.wrangler/ +.github/workflows/old/ +dependencies/ + +emojis.json +UnicodeData.txt +meson-info/ +meson-logs/ +meson-private/ +*.o +.ninja_* +build.ninja +compile_commands.json + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +.vscode/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +*.Designer.cs.dll +unsuccessfulbuild +*.lastbuildstate +*.psess +*.vsp +*.vspx +*.tlog + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msm +*.msp +*.lnk + +# Clangd cache +/.cache/clangd +# =========================================== +# LOCAL FILES - DO NOT COMMIT +# These files contain local development context +# and should never be pushed to the repository +# =========================================== +memory-bank/ +.clangd +compile_commands.json +ci-old.yml +openssl-* +_CPack_Packages/ +/install/ \ No newline at end of file diff --git a/archived/projt-launcher/.markdownlint.yaml b/archived/projt-launcher/.markdownlint.yaml new file mode 100644 index 0000000000..25d790391c --- /dev/null +++ b/archived/projt-launcher/.markdownlint.yaml @@ -0,0 +1,19 @@ +# MD013/line-length - Line length +MD013: false + +# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content +MD024: + siblings-only: true + +# MD025/single-title/single-h1 - Multiple top-level headings in the same document +MD025: + front_matter_title: "" + +# MD026/no-trailing-punctuation - Trailing punctuation in heading +MD026: false + +# MD033/no-inline-html Inline HTML +MD033: false + +# MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading +MD041: false diff --git a/archived/projt-launcher/.markdownlintignore b/archived/projt-launcher/.markdownlintignore new file mode 100644 index 0000000000..74758cf60c --- /dev/null +++ b/archived/projt-launcher/.markdownlintignore @@ -0,0 +1,29 @@ +# build / generated +_site/ +node_modules/ + +# vendored / forked upstreams +bzip2/ +quazip/ +tomlplusplus/ +libnbtplusplus/ +libqrencode/ +cmark/ +extra-cmake-modules/ +zlib/ +qt/ +gamemode/ +json/ +libpng/ +toolchain/ + +# generated reports +TODO_FIXME_REPORT.md + +docs +website-root/handbook + +launcher/resources/documents/manifesto.md +flatpak/README.md +ptinstaller/ +CHANGELOG.md diff --git a/archived/projt-launcher/.python-version b/archived/projt-launcher/.python-version new file mode 100644 index 0000000000..c8cfe39591 --- /dev/null +++ b/archived/projt-launcher/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/archived/projt-launcher/CHANGELOG.md b/archived/projt-launcher/CHANGELOG.md new file mode 100644 index 0000000000..d5f94e1144 --- /dev/null +++ b/archived/projt-launcher/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +## 0.0.5-1 DRAFT + +**Date range:** 2026-01-16 to yyyy-mm-dd + +We are excited to announce the official release of **ProjT Launcher: version 0.0.5-1**. + +This release strengthens version compliance, particularly by improving Fabric/Quilt and LWJGL +component parsing; it also adds a Launcher Hub (web-based dashboard) and makes the packaging flow +(RPM, AppImage, portable, macOS) more consistent. The CI and build systems (Qt/CMake, MSYS2/MSVC +Clang) have been simplified and stabilized, while multi-platform compilation, DESTDIR placement, +and metadata-related incompatibilities have been addressed. + +### Highlights +- Improved Fabric/Quilt component version resolution with better Minecraft-version alignment. +- Added Launcher Hub support (web-based dashboard). +- Strengthened version comparison logic, especially for release-candidate handling. +- Added a compatibility hotfix for LWJGL metadata variants. +- Added Modrinth collection import for existing instances. +- Switched the Linux Launcher Hub backend from QtWebEngine to CEF and added a native cockpit dashboard. +- Improved CEF packaging/runtime handling for Nix, AppDir, and multi-architecture builds. +- Reduced launcher/build warnings and resolved zlib/libpng symbol-conflict issues. + +### Added +- Launcher Hub feature (web-based panel). +- New unit tests for various launcher components. +- More complete packaging outputs across platforms, especially Linux/macOS artifact flow. +- Modrinth collection import flow in the existing mod download dialog for current instances. +- Native cockpit dashboard for Launcher Hub with quick actions and Linux fallback support. +- Linux CEF Hub backend wiring, including local SDK detection and configure-time auto-download support. +- Additional standalone unit tests for utility, JSON, filtering, serialization, and exponential-series behavior. +- AI Usage Policy, refreshed licensing display/docs, and new GPLv3 program-info asset. + +### Changed +- Component dependency resolution flow (`ComponentUpdateTask`) is now more stable. +- Qt/CMake-based build and preset flows are more consistent across Linux, Windows, and macOS. +- Improved MSYS2/MSVC/Clang compatibility for Windows builds. +- Reorganized packaging architecture (RPM/portable/AppImage/macOS artifacts). +- Linux Hub now uses CEF instead of QtWebEngine, while keeping platform-specific backends on Windows and macOS. +- CEF build and packaging flow is now architecture-aware (`x64`/`arm64`) and more reproducible on Nix. +- CEF source builds remain optional by default during configure. +- zlib symbol handling was refined to use libpng-targeted shim overrides instead of global prefixing. +- About dialog licensing/contributing content and related program-info metadata were refreshed. + +### Fixed +- Fixed metadata and version compatibility issues related to Fabric/LWJGL. +- Fixed path/folder coverage and filesystem test issues. +- Fixed `DESTDIR` and library placement issues in AppImage/portable packages. +- Fixed multiple macOS/Windows build and linking incompatibilities. +- Fixed Linux CEF runtime installation so AppDir packaging includes the required binaries and resources. +- Fixed Nix-side CEF runtime linking, runtime dependency propagation, and translation model reset behavior. +- Fixed MinGW-specific build problems, including CFG flag incompatibility and `DataPackPage` LTO linker errors. +- Fixed Linux CEF command-line switch handling for `disable-features`. +- Fixed bundled zlib/libpng symbol conflicts and corrected installed `pkg-config` prefix resolution. +- Fixed remaining launcher null-dereference/container-access warnings and reduced general build-warning noise. +- Fixed NeoForge legacy URL normalization and `/releases` Maven path handling. + +### Internal / CI +- CI workflows were simplified and reorganized for GitLab/GitHub. +- Removed old/duplicated workflows; improved fuzzing and packaging steps. +- Updated maintenance automation for subtree/toolchain/Qt management. +- Expanded targeted standalone test coverage and kept launcher test targets aligned with shared runtime deps. +- Refined Linux image/Flatpak/runner workflow details and cleaned up repository metadata/docs housekeeping. +- Debounced instance directory reloads to reduce noisy logs and unnecessary refresh churn. diff --git a/archived/projt-launcher/CMakeLists.txt b/archived/projt-launcher/CMakeLists.txt new file mode 100644 index 0000000000..4061519822 --- /dev/null +++ b/archived/projt-launcher/CMakeLists.txt @@ -0,0 +1,854 @@ +cmake_minimum_required(VERSION 3.25) # Required for features like `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` + +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() +if(POLICY CMP0177) + cmake_policy(SET CMP0177 NEW) +endif() + +project(Launcher) + +# Force bzip2 to build static library (required for quazip) +set(ENABLE_STATIC_LIB ON CACHE BOOL "Enable static lib for bzip2" FORCE) + +# Set default build type for single-config generators only. +if(CMAKE_CONFIGURATION_TYPES) + if(DEFINED CMAKE_BUILD_TYPE) + unset(CMAKE_BUILD_TYPE CACHE) + endif() +else() + if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE) + endif() +endif() + +string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) +if(IS_IN_SOURCE_BUILD) + message(FATAL_ERROR "You are building the Launcher in-source. Please separate the build tree from the source tree.") +endif() + + + +##################################### Set CMake options #################################### +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") + +# Output layout +set(Launcher_OUTPUT_ROOT "${PROJECT_BINARY_DIR}/out" CACHE PATH "Root output directory for build artifacts") +if(DEFINED ENV{GITHUB_ACTIONS}) + file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" _projt_src_dir) + file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}" _projt_bin_dir) + string(REGEX REPLACE "([][+.*^$()\\\\|?])" "\\\\\\1" _projt_src_dir_esc "${_projt_src_dir}") + string(REGEX REPLACE "([][+.*^$()\\\\|?])" "\\\\\\1" _projt_bin_dir_esc "${_projt_bin_dir}") + if(NOT Launcher_OUTPUT_ROOT MATCHES "^${_projt_src_dir_esc}(/|$)" + AND NOT Launcher_OUTPUT_ROOT MATCHES "^${_projt_bin_dir_esc}(/|$)") + set(Launcher_OUTPUT_ROOT "${PROJECT_BINARY_DIR}/out" CACHE PATH "Root output directory for build artifacts" FORCE) + endif() + unset(_projt_src_dir) + unset(_projt_bin_dir) + unset(_projt_src_dir_esc) + unset(_projt_bin_dir_esc) +endif() +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${Launcher_OUTPUT_ROOT}/root/$") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${Launcher_OUTPUT_ROOT}/root/$") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${Launcher_OUTPUT_ROOT}/root/$") +set(CMAKE_JAVA_TARGET_OUTPUT_DIR "${Launcher_OUTPUT_ROOT}/root/jars") + +macro(projt_push_output_dirs name) + set(_PROJT_PREV_RUNTIME "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") + set(_PROJT_PREV_LIBRARY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") + set(_PROJT_PREV_ARCHIVE "${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}") + set(_PROJT_PREV_JAVA "${CMAKE_JAVA_TARGET_OUTPUT_DIR}") + set(_projt_dir "${Launcher_OUTPUT_ROOT}/${name}") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${_projt_dir}/$") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${_projt_dir}/$") + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${_projt_dir}/$") + set(CMAKE_JAVA_TARGET_OUTPUT_DIR "${_projt_dir}/jars") +endmacro() + +macro(projt_pop_output_dirs) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${_PROJT_PREV_RUNTIME}") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${_PROJT_PREV_LIBRARY}") + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${_PROJT_PREV_ARCHIVE}") + set(CMAKE_JAVA_TARGET_OUTPUT_DIR "${_PROJT_PREV_JAVA}") + unset(_projt_dir) +endmacro() + +macro(projt_push_install_libdir new_libdir) + set(_PROJT_PREV_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}") + set(_PROJT_PREV_INSTALL_FULL_LIBDIR "${CMAKE_INSTALL_FULL_LIBDIR}") + set(CMAKE_INSTALL_LIBDIR "${new_libdir}") + if(IS_ABSOLUTE "${new_libdir}") + set(CMAKE_INSTALL_FULL_LIBDIR "${new_libdir}") + else() + set(CMAKE_INSTALL_FULL_LIBDIR "${CMAKE_INSTALL_PREFIX}/${new_libdir}") + endif() +endmacro() + +macro(projt_pop_install_libdir) + set(CMAKE_INSTALL_LIBDIR "${_PROJT_PREV_INSTALL_LIBDIR}") + set(CMAKE_INSTALL_FULL_LIBDIR "${_PROJT_PREV_INSTALL_FULL_LIBDIR}") + unset(_PROJT_PREV_INSTALL_LIBDIR) + unset(_PROJT_PREV_INSTALL_FULL_LIBDIR) +endmacro() + +macro(projt_push_install_includedir new_includedir) + set(_PROJT_PREV_INSTALL_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}") + set(_PROJT_PREV_INSTALL_FULL_INCLUDEDIR "${CMAKE_INSTALL_FULL_INCLUDEDIR}") + set(CMAKE_INSTALL_INCLUDEDIR "${new_includedir}") + if(IS_ABSOLUTE "${new_includedir}") + set(CMAKE_INSTALL_FULL_INCLUDEDIR "${new_includedir}") + else() + set(CMAKE_INSTALL_FULL_INCLUDEDIR "${CMAKE_INSTALL_PREFIX}/${new_includedir}") + endif() +endmacro() + +macro(projt_pop_install_includedir) + set(CMAKE_INSTALL_INCLUDEDIR "${_PROJT_PREV_INSTALL_INCLUDEDIR}") + set(CMAKE_INSTALL_FULL_INCLUDEDIR "${_PROJT_PREV_INSTALL_FULL_INCLUDEDIR}") + unset(_PROJT_PREV_INSTALL_INCLUDEDIR) + unset(_PROJT_PREV_INSTALL_FULL_INCLUDEDIR) +endmacro() + +macro(projt_push_install_libexecdir new_libexecdir) + set(_PROJT_PREV_INSTALL_LIBEXECDIR "${CMAKE_INSTALL_LIBEXECDIR}") + set(_PROJT_PREV_INSTALL_FULL_LIBEXECDIR "${CMAKE_INSTALL_FULL_LIBEXECDIR}") + set(CMAKE_INSTALL_LIBEXECDIR "${new_libexecdir}") + if(IS_ABSOLUTE "${new_libexecdir}") + set(CMAKE_INSTALL_FULL_LIBEXECDIR "${new_libexecdir}") + else() + set(CMAKE_INSTALL_FULL_LIBEXECDIR "${CMAKE_INSTALL_PREFIX}/${new_libexecdir}") + endif() +endmacro() + +macro(projt_pop_install_libexecdir) + set(CMAKE_INSTALL_LIBEXECDIR "${_PROJT_PREV_INSTALL_LIBEXECDIR}") + set(CMAKE_INSTALL_FULL_LIBEXECDIR "${_PROJT_PREV_INSTALL_FULL_LIBEXECDIR}") + unset(_PROJT_PREV_INSTALL_LIBEXECDIR) + unset(_PROJT_PREV_INSTALL_FULL_LIBEXECDIR) +endmacro() + +macro(projt_push_autogen_disabled) + set(_PROJT_PREV_AUTOMOC "${CMAKE_AUTOMOC}") + set(_PROJT_PREV_AUTORCC "${CMAKE_AUTORCC}") + set(_PROJT_PREV_AUTOUIC "${CMAKE_AUTOUIC}") + set(CMAKE_AUTOMOC OFF) + set(CMAKE_AUTORCC OFF) + set(CMAKE_AUTOUIC OFF) +endmacro() + +macro(projt_pop_autogen_disabled) + set(CMAKE_AUTOMOC "${_PROJT_PREV_AUTOMOC}") + set(CMAKE_AUTORCC "${_PROJT_PREV_AUTORCC}") + set(CMAKE_AUTOUIC "${_PROJT_PREV_AUTOUIC}") + unset(_PROJT_PREV_AUTOMOC) + unset(_PROJT_PREV_AUTORCC) + unset(_PROJT_PREV_AUTOUIC) +endmacro() + +if(UNIX AND NOT APPLE) + include(GNUInstallDirs) + set(_launcher_default_bundled_libdir "${CMAKE_INSTALL_LIBDIR}/projtlauncher") + set(Launcher_BUNDLED_LIBDIR "${_launcher_default_bundled_libdir}" CACHE STRING "Install directory for bundled libraries (relative to prefix). Set empty to disable.") + set(_launcher_default_bundled_includedir "include/projtlauncher") + set(Launcher_BUNDLED_INCLUDEDIR "${_launcher_default_bundled_includedir}" CACHE STRING "Install directory for bundled headers (relative to prefix). Set empty to disable.") + set(_launcher_default_bundled_libexecdir "libexec/projtlauncher") + set(Launcher_BUNDLED_LIBEXECDIR "${_launcher_default_bundled_libexecdir}" CACHE STRING "Install directory for bundled libexec tools (relative to prefix). Set empty to disable.") + set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME Runtime) + unset(_launcher_default_bundled_libdir) + unset(_launcher_default_bundled_includedir) + unset(_launcher_default_bundled_libexecdir) +endif() + +######## Set compiler flags ######## +set(CMAKE_CXX_STANDARD_REQUIRED true) +set(CMAKE_C_STANDARD_REQUIRED true) +set(CMAKE_CXX_STANDARD 23) + +if(MSVC) +set(CMAKE_C_STANDARD 11) +else() +set(CMAKE_C_STANDARD 23) +endif() + +include(GenerateExportHeader) +add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x060400) +add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) + +if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + add_compile_options( + # /GS Adds buffer security checks, default on but included anyway to mirror gcc's fstack-protector flag + "$<$:/GS>" + + # /Gw helps reduce binary size + # /Gy allows the compiler to package individual functions + # /guard:cf enables control flow guard + "$<$,$>:/Gw;/Gy;/guard:cf>" + ) + + add_link_options( + # /LTCG allows for linking wholy optimizated programs + # /MANIFEST:NO disables generating a manifest file, we instead provide our own + # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB + "$<$:/LTCG;/MANIFEST:NO;/STACK:8388608>" + ) + + # /GL enables whole program optimizations + # NOTE: With Clang, this is implemented as regular LTO and only used during linking + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options("$<$,$>:/GL>") + endif() + + # TODO(@YongDo-Hyun): Is sccache affected by this? Would be nice to use `ProgramDatabase`.... + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") + + # Use static runtime for MSVC to make the app portable (avoids missing VCRUNTIME140.dll etc.) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + + if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDLL") + set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release "") + set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release "") + endif() +else() + add_compile_options("$<$:-Wall;-pedantic;-fstack-protector-strong;--param=ssp-buffer-size=4>") + + # ATL's pack list needs more than the default 1 Mib stack on windows + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_link_options("$<$:-Wl,--stack,8388608>") + + # Emit PDBs for WinDbg, etc. + if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + set(CMAKE_EXE_LINKER_FLAGS "-Wl,--pdb= ${CMAKE_EXE_LINKER_FLAGS}") + + # TODO: Look into -gc-sections to further reduce binary size + add_compile_options("$<$,$>:-ffunction-sections;-fdata-sections;-mguard=cf>") + endif() + + # -ffunction-sections and -fdata-sections help reduce binary size + # -mguard=cf enables Control Flow Guard when the MinGW frontend supports it + # -Wl,--gc-sections removes unused sections during linking + foreach(lang C CXX) + set(_projt_release_flags "-ffunction-sections -fdata-sections") + if(CMAKE_${lang}_COMPILER_ID STREQUAL "Clang") + string(APPEND _projt_release_flags " -mguard=cf") + endif() + set("CMAKE_${lang}_FLAGS_RELEASE" "${_projt_release_flags}") + unset(_projt_release_flags) + endforeach() + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "-Wl,--gc-sections ${CMAKE_EXE_LINKER_FLAGS_RELEASE}") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-Wl,--gc-sections ${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") + elseif(APPLE) + # macOS specific dead code stripping + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "-Wl,-dead_strip ${CMAKE_EXE_LINKER_FLAGS_RELEASE}") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-Wl,-dead_strip ${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") + elseif(UNIX) + # Linux/BSD dead code stripping + foreach(lang C CXX) + set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections") + endforeach() + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "-Wl,--gc-sections ${CMAKE_EXE_LINKER_FLAGS_RELEASE}") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-Wl,--gc-sections ${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") + endif() +endif() + +# Fix aarch64 build for toml++ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") + +# set release flags for build targets +set(CMAKE_C_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") + +# Export compile commands for debug builds if we can (useful in LSPs like clangd) +# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html +if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR MATCHES "^Ninja") + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +endif() + +if(UNIX AND NOT APPLE) + # Keep install RPATHs relative so RPM check-rpaths doesn't reject build-root paths. + set(_launcher_rpath_entries "") + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + list(APPEND _launcher_rpath_entries "$ORIGIN/../${Launcher_BUNDLED_LIBDIR}") + endif() + list(APPEND _launcher_rpath_entries "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}" "$ORIGIN") + string(JOIN ":" CMAKE_INSTALL_RPATH ${_launcher_rpath_entries}) + unset(_launcher_rpath_entries) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) + set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) +endif() + +option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) + +# If this is a Debug build turn on address sanitiser +if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") AND DEBUG_ADDRESS_SANITIZER) + message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") + if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + # using clang with clang-cl front end + message(STATUS "Address Sanitizer available on Clang MSVC frontend") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") + else() + # AppleClang and Clang + message(STATUS "Address Sanitizer available on Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") + endif() + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + # GCC + message(STATUS "Address Sanitizer available on GCC") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") + link_libraries("asan") + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") + message(STATUS "Address Sanitizer available on MSVC") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") + else() + message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") + endif() +endif() + + +option(ENABLE_LTO "Enable Link Time Optimization" off) + +if(ENABLE_LTO) + include(CheckIPOSupported) + check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) + + if(ipo_supported) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE) + if(CMAKE_BUILD_TYPE) + if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + message(STATUS "IPO / LTO enabled") + else() + message(STATUS "Not enabling IPO / LTO on debug builds") + endif() + else() + message(STATUS "IPO / LTO will only be enabled for release builds") + endif() + else() + message(STATUS "IPO / LTO not supported: <${ipo_error}>") + endif() +endif() + +option(BUILD_TESTING "Build the testing tree." ON) +option(BUILD_FUZZERS "Build fuzzing targets." OFF) +option(Launcher_ALLOW_FETCHCONTENT "Allow FetchContent fallback for tomlplusplus" OFF) +option(Launcher_ALLOW_ECM_FETCHCONTENT "Allow FetchContent fallback for ECM" OFF) +option(Launcher_SHOW_CMAKE_WARNINGS "Show CMake developer/deprecated warnings from dependencies" ON) + +if(Launcher_SHOW_CMAKE_WARNINGS) + set(CMAKE_WARN_DEPRECATED ON) + set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS OFF) +endif() + +macro(launcher_fetchcontent_makeavailable content_name) + if(Launcher_SHOW_CMAKE_WARNINGS) + FetchContent_MakeAvailable(${content_name}) + else() + set(launcher_warn_deprecated_set FALSE) + if(DEFINED CMAKE_WARN_DEPRECATED) + set(launcher_warn_deprecated_value "${CMAKE_WARN_DEPRECATED}") + set(launcher_warn_deprecated_set TRUE) + endif() + set(launcher_suppress_dev_set FALSE) + if(DEFINED CMAKE_SUPPRESS_DEVELOPER_WARNINGS) + set(launcher_suppress_dev_value "${CMAKE_SUPPRESS_DEVELOPER_WARNINGS}") + set(launcher_suppress_dev_set TRUE) + endif() + + set(CMAKE_WARN_DEPRECATED OFF) + set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS ON) + FetchContent_MakeAvailable(${content_name}) + + if(launcher_warn_deprecated_set) + set(CMAKE_WARN_DEPRECATED "${launcher_warn_deprecated_value}") + else() + unset(CMAKE_WARN_DEPRECATED) + endif() + if(launcher_suppress_dev_set) + set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS "${launcher_suppress_dev_value}") + else() + unset(CMAKE_SUPPRESS_DEVELOPER_WARNINGS) + endif() + unset(launcher_warn_deprecated_set) + unset(launcher_warn_deprecated_value) + unset(launcher_suppress_dev_set) + unset(launcher_suppress_dev_value) + endif() +endmacro() + +message(STATUS "Using bundled extra-cmake-modules") +set(ECM_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../extra-cmake-modules/modules") +set(ECM_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../extra-cmake-modules/modules") +# Add kde-modules for KDEInstallDirs and other KDE modules +list(APPEND ECM_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../extra-cmake-modules/kde-modules") +set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") + +include(CTest) +include(ECMAddTests) + +if(BUILD_TESTING) + enable_testing() +endif() + +##################################### Set Application options ##################################### + +######## Set URLs ######## +set(Launcher_NEWS_RSS_URL "https://projecttick.org/product/projt-launcher/feed.xml" CACHE STRING "URL to fetch ProjT Launcher's news RSS feed from.") +set(Launcher_NEWS_OPEN_URL "https://projecttick.org/product/projt-launcher/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") +set(Launcher_WIKI_URL "https://projecttick.org/handbook/" CACHE STRING "URL that gets opened when the user clicks 'Launcher Help'") +set(Launcher_HELP_URL "https://projecttick.org/handbook/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window") +set(Launcher_HUB_HOME_URL "https://projecttick.org/p/projt-launcher/" CACHE STRING "Launcher Hub home URL") +set(Launcher_HUB_COMMUNITY_URL "https://projecttick.org/projtlauncher/discord" CACHE STRING "Launcher Hub community URL") +set(Launcher_HUB_SEARCH_URL "https://www.google.com/search?q=%1" CACHE STRING "Search URL (with %1 placeholder) used by the Launcher Hub address bar") +set(Launcher_LOGIN_CALLBACK_URL "https://projecttick.org/p/projt-launcher" CACHE STRING "URL that gets opened when the user successfully logins.") +set(Launcher_FMLLIBS_BASE_URL "https://files.projecttick.org/fmllibs/" CACHE STRING "URL for FML Libraries.") + +######## Set version numbers ######## +set(Launcher_VERSION_MAJOR 0) +set(Launcher_VERSION_MINOR 0) +set(Launcher_VERSION_PATCH 5) +set(Launcher_VERSION_TWEAK 1) + +set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.${Launcher_VERSION_TWEAK}") +set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.${Launcher_VERSION_TWEAK}") +set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},${Launcher_VERSION_TWEAK}") + +# Build platform. +set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") + +# CI fuzz builds use a reduced target set; skip heavy components and installers when enabled. +option(LAUNCHER_FUZZ_ONLY "Build only fuzzing targets (skips installers, Java launcher, and desktop integration)" OFF) + +# Release feed URL for updater +set(Launcher_UPDATER_RELEASES_URL "https://projecttick.org/product/projt-launcher/feed.xml" CACHE STRING "URL for the updater release feed manifest.") + +# Name to help updater identify valid artifacts +set(Launcher_BUILD_ARTIFACT "" CACHE STRING "Artifact name to help the updater identify valid artifacts.") + +# The metadata server +set(Launcher_META_URL "https://meta.projecttick.org/" CACHE STRING "URL to fetch Launcher's meta files from.") + +# Imgur API Client ID +set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application") + +# Bug tracker URL +set(Launcher_BUG_TRACKER_URL "https://github.com/Project-Tick/ProjT-Launcher/issues" CACHE STRING "URL for the bug tracker.") + +# Translations Platform URL +set(Launcher_TRANSLATIONS_URL "https://crowdin.com/project/projtlauncher" CACHE STRING "URL for the translations platform.") +set(Launcher_TRANSLATION_FILES_URL "https://i18n.projecttick.org/" CACHE STRING "URL for the translations files.") + +# Matrix Space +set(Launcher_MATRIX_URL "https://projecttick.org/projtlauncher/matrix" CACHE STRING "URL to the Matrix Space") + +# Discord URL +set(Launcher_DISCORD_URL "https://projecttick.org/projtlauncher/discord" CACHE STRING "URL for the Discord guild.") + +# Subreddit URL +set(Launcher_SUBREDDIT_URL "https:/projecttick.org/projtlauncher/reddit" CACHE STRING "URL for the subreddit.") + +# Builds +set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") + +# Java downloader +set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) + +# Although we recommend enabling this, we cannot guarantee binary compatibility on +# differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this +# feature if they know it will work with their distribution. +if(UNIX AND NOT APPLE) + set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) +endif() + +# Java downloader +option(Launcher_ENABLE_JAVA_DOWNLOADER "Build the java downloader feature" ${Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT}) + +# Native libraries +if(UNIX AND APPLE) + set(Launcher_GLFW_LIBRARY_NAME "libglfw.dylib" CACHE STRING "Name of native glfw library") + set(Launcher_OPENAL_LIBRARY_NAME "libopenal.dylib" CACHE STRING "Name of native openal library") +elseif(UNIX) + set(Launcher_GLFW_LIBRARY_NAME "libglfw.so" CACHE STRING "Name of native glfw library") + set(Launcher_OPENAL_LIBRARY_NAME "libopenal.so" CACHE STRING "Name of native openal library") +elseif(WIN32) + set(Launcher_GLFW_LIBRARY_NAME "glfw.dll" CACHE STRING "Name of native glfw library") + set(Launcher_OPENAL_LIBRARY_NAME "OpenAL.dll" CACHE STRING "Name of native openal library") +endif() + +# API Keys +# NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service +# of these platforms, please change these API keys beforehand. +# Be aware that if you were to use these API keys for malicious purposes they might get revoked, which might cause +# breakage to thousands of users. +# If you don't plan to use these features of this software, you can just remove these values. + +# By using this key in your builds you accept the terms of use laid down in +# https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use +set(Launcher_MSA_CLIENT_ID "3035382c-8f73-493a-b579-d182905c2864" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") + +# By using this key in your builds you accept the terms and conditions laid down in +# https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions +# NOTE: CurseForge requires you to change this if you make any kind of derivative work. +# This key was issued specifically for ProjT Launcher +set(Launcher_CURSEFORGE_API_KEY "$2a$10$S7KcKijbCj8mCHUQcn0tgOmtHg0kA8q9FI0niNJJ7knPq0INomzrG" CACHE STRING "API key for the CurseForge platform") + +set(Launcher_COMPILER_NAME ${CMAKE_CXX_COMPILER_ID}) +set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION}) +set(Launcher_COMPILER_TARGET_SYSTEM ${CMAKE_SYSTEM_NAME}) +set(Launcher_COMPILER_TARGET_SYSTEM_VERSION ${CMAKE_SYSTEM_VERSION}) +set(Launcher_COMPILER_TARGET_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR}) + +#### Check the current Git commit and branch +include(GetGitRevisionDescription) +git_get_exact_tag(Launcher_GIT_TAG) +get_git_head_revision(Launcher_GIT_REFSPEC Launcher_GIT_COMMIT) + +message(STATUS "Git commit: ${Launcher_GIT_COMMIT}") +message(STATUS "Git tag: ${Launcher_GIT_TAG}") +message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}") + +string(TIMESTAMP TODAY "%Y-%m-%d") +set(Launcher_BUILD_TIMESTAMP "${TODAY}") + +################################ 3rd Party Libs ################################ + +if(WIN32 AND NOT MINGW) + # Detect NuGet architecture + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|arm64") + set(NUGET_ARCH "arm64") + set(OSSL_ARCH "arm64") + else() + set(NUGET_ARCH "x64") + set(OSSL_ARCH "x64") + endif() + + include(findOpenSSLbyNuget) + projt_find_openssl_by_nuget() + + list(APPEND CMAKE_PREFIX_PATH "${PTLIBZIPPY_ROOT}" "${OPENSSL_ROOT_DIR}") + list(APPEND CMAKE_INCLUDE_PATH "${CMAKE_SOURCE_DIR}/dependencies") +endif() + +include(usePTlibzippy) +projt_add_ptlibzippy() + +if(DEFINED PTlibzippy_SOURCE_DIR AND DEFINED PTlibzippy_BINARY_DIR) + # Make PTlibzippy headers available to libpng config generation. + set(ZLIB_INCLUDE_DIRS "${PTlibzippy_SOURCE_DIR};${PTlibzippy_BINARY_DIR}") +elseif(PTLIBZIPPY_INCLUDE_DIRS) + set(ZLIB_INCLUDE_DIRS "${PTLIBZIPPY_INCLUDE_DIRS}") +endif() + +if(NOT LAUNCHER_FUZZ_ONLY AND UNIX AND NOT APPLE) + include(useLibpng) + projt_add_libpng() +endif() + +if(NOT LAUNCHER_FUZZ_ONLY) + if(NOT OpenSSL_FOUND) + find_package(OpenSSL REQUIRED) + endif() + + set(LAUNCHER_WITH_WEBENGINE ON) + + if(MINGW) + set(LAUNCHER_WITH_WEBENGINE OFF) + endif() + if(UNIX AND NOT APPLE) + set(LAUNCHER_WITH_WEBENGINE OFF) + endif() + if(MSVC AND (CMAKE_VS_PLATFORM_NAME STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")) + set(LAUNCHER_WITH_WEBENGINE OFF) + endif() + + set(QT_COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL) + if(LAUNCHER_WITH_WEBENGINE) + list(APPEND QT_COMPONENTS WebEngineWidgets WebChannel) + endif() + + find_package(Qt6 REQUIRED COMPONENTS ${QT_COMPONENTS}) + if(DEFINED Qt6Core_VERSION) + set(_projt_qt_version "${Qt6Core_VERSION}") + elseif(DEFINED Qt6_VERSION) + set(_projt_qt_version "${Qt6_VERSION}") + endif() + if(DEFINED _projt_qt_version AND _projt_qt_version VERSION_LESS "6.8.0") + message(FATAL_ERROR "Qt 6.8.0 or newer is required. Detected: ${_projt_qt_version}") + endif() + unset(_projt_qt_version) + find_package(Qt6 COMPONENTS DBus) + list(APPEND Launcher_QT_DBUS Qt6::DBus) +endif() +# Note: Qt6 removed from fuzzer builds - fuzz_qjson_parse disabled due to glib dependency issues + +# System / Package Manager Provided Libs - Using bundled versions +if(NOT LAUNCHER_FUZZ_ONLY) + include(useBZip2) + projt_add_bzip2() + + if(UNIX AND NOT APPLE) + include(useMinizip) + projt_add_minizip() + endif() + + include(useQuazip) + projt_add_quazip() + + include(useTomlplusplus) + projt_add_tomlplusplus() +endif() + +if(NOT LAUNCHER_FUZZ_ONLY) + include(useCMark) + projt_add_cmark() +endif() + +if(NOT LAUNCHER_FUZZ_ONLY) + include(useLibqrencode) + projt_add_libqrencode() +endif() + +if(NOT LAUNCHER_FUZZ_ONLY) + include(ECMQtDeclareLoggingCategory) +endif() + +####################################### Program Info ####################################### + +if(NOT LAUNCHER_FUZZ_ONLY) + projt_push_output_dirs("program_info") + add_subdirectory(program_info) + projt_pop_output_dirs() +endif() + +####################################### Install layout ####################################### + +set(Launcher_ENABLE_UPDATER NO) +set(Launcher_BUILD_UPDATER NO) + +if (NOT APPLE AND (NOT Launcher_UPDATER_RELEASES_URL STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL "")) + set(Launcher_BUILD_UPDATER YES) +endif() + +if(NOT LAUNCHER_FUZZ_ONLY) + if(NOT (UNIX AND APPLE)) + # Install "portable.txt" if selected component is "portable" + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION "." COMPONENT portable EXCLUDE_FROM_ALL) + endif() +endif() + +if(NOT LAUNCHER_FUZZ_ONLY AND UNIX AND APPLE) + set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(FRAMEWORK_DEST_DIR "${Launcher_Name}.app/Contents/Frameworks") + set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") + set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") + + # Mac bundle settings + set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}") + set(MACOSX_BUNDLE_INFO_STRING "${Launcher_DisplayName}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.projecttick.${Launcher_Name}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_NAME}") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}") + set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}") + set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) + set(MACOSX_BUNDLE_COPYRIGHT "${Launcher_Copyright_Mac}") + set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "Lju8CtOHHDmev7KkEl3ks/6wWkG1mlZ0G89XIG8hNLQ=" CACHE STRING "Public key for Sparkle update feed") + set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://projecttick.org/product/projt-launcher/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") + set(MACOSX_BOOTSTRAP_FEED_URL "https://projecttick.org/product/projt-launcher/appcast.xml" CACHE STRING "URL for bootstrap update feed") + set(MACOSX_BOOTSTRAP_URL_TEMPLATE "https://github.com/Project-Tick/ProjT-Launcher/releases/download/%s/ProjTLauncher-macOS-%s.zip" CACHE STRING "URL template for bootstrap download") + + set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz" CACHE STRING "URL to Sparkle release archive") + set(MACOSX_SPARKLE_SHA256 "fd5681ee92bf238aaac2d08214ceaf0cc8976e452d7f882d80bac1e61581f3b1" CACHE STRING "SHA256 checksum for Sparkle release archive") + set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + + if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "") + set(Launcher_ENABLE_UPDATER YES) + endif() + + # Add the icon + install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) + + if(NOT LAUNCHER_FUZZ_ONLY) + add_subdirectory(bootstrap/macos) + find_program(ACTOOL_EXE actool DOC "Path to the apple asset catalog compiler") + if(ACTOOL_EXE) + execute_process( + COMMAND xcodebuild -version + OUTPUT_VARIABLE XCODE_VERSION_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + string(REGEX MATCH "Xcode ([0-9]+\\.[0-9]+)" XCODE_VERSION_MATCH "${XCODE_VERSION_OUTPUT}") + if(XCODE_VERSION_MATCH) + set(XCODE_VERSION ${CMAKE_MATCH_1}) + else() + set(XCODE_VERSION 0.0) + endif() + + if(XCODE_VERSION VERSION_GREATER_EQUAL 26.0) + set(ASSETS_OUT_DIR "${CMAKE_BINARY_DIR}/program_info") + set(GENERATED_ASSETS_CAR "${ASSETS_OUT_DIR}/Assets.car") + set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Branding_MAC_ICON}") + + add_custom_command( + OUTPUT "${GENERATED_ASSETS_CAR}" + COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" + --compile "${ASSETS_OUT_DIR}" + --output-partial-info-plist /dev/null + --app-icon ${Launcher_Name} + --enable-on-demand-resources NO + --target-device mac + --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} + --platform macosx + DEPENDS "${ICON_SOURCE}" + COMMENT "Compiling asset catalog (${ICON_SOURCE})" + VERBATIM + ) + add_custom_target(compile_assets ALL DEPENDS "${GENERATED_ASSETS_CAR}") + install(FILES "${GENERATED_ASSETS_CAR}" DESTINATION "${RESOURCES_DEST_DIR}") + else() + message(WARNING "Xcode ${XCODE_VERSION} is too old. Minimum required version is 26.0. Not compiling liquid glass icons.") + endif() + + else() + message(WARNING "actool not found. Cannot compile macOS app icons.\n" + "Install Xcode command line tools: 'xcode-select --install'") + endif() + endif() + +elseif(NOT LAUNCHER_FUZZ_ONLY AND UNIX) + foreach(_launcher_install_dir IN ITEMS + BINDIR + SBINDIR + LIBDIR + LIBEXECDIR + INCLUDEDIR + LOCALSTATEDIR + SHAREDSTATEDIR + DATAROOTDIR + DATADIR + LOCALEDIR + MANDIR + INFODIR + SYSCONFDIR + RUNSTATEDIR + DOCDIR) + if(NOT DEFINED KDE_INSTALL_${_launcher_install_dir} + AND DEFINED CMAKE_INSTALL_${_launcher_install_dir} + AND NOT CMAKE_INSTALL_${_launcher_install_dir} STREQUAL "") + set(KDE_INSTALL_${_launcher_install_dir} "${CMAKE_INSTALL_${_launcher_install_dir}}" CACHE PATH "" FORCE) + endif() + endforeach() + unset(_launcher_install_dir) + include(KDEInstallDirs) + + set(BINARY_DEST_DIR "bin") + set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") + set(JARS_DEST_DIR "share/${Launcher_Name}") + + # Set RPATH (include bundled lib dir if configured) + set(_launcher_rpath_entries "") + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + list(APPEND _launcher_rpath_entries "$ORIGIN/../${Launcher_BUNDLED_LIBDIR}") + endif() + list(APPEND _launcher_rpath_entries "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}" "$ORIGIN") + string(JOIN ":" Launcher_BINARY_RPATH ${_launcher_rpath_entries}) + unset(_launcher_rpath_entries) + + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${KDE_INSTALL_APPDIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) + + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") + + set(PLUGIN_DEST_DIR "plugins") + set(BUNDLE_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") + + if(Launcher_ManPage) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") + endif() + + # Install basic runner script if component "portable" is selected + configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) + install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION "." RENAME ${Launcher_Name} COMPONENT portable EXCLUDE_FROM_ALL) + +elseif(WIN32) + set(BINARY_DEST_DIR ".") + set(LIBRARY_DEST_DIR ".") + set(PLUGIN_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") + set(JARS_DEST_DIR "jars") +elseif(LAUNCHER_FUZZ_ONLY) + # For fuzz-only builds on other platforms, skip installer/layout specifics. + set(BINARY_DEST_DIR ".") + set(LIBRARY_DEST_DIR ".") + set(PLUGIN_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") + set(JARS_DEST_DIR "jars") +else() + message(FATAL_ERROR "Platform not supported") +endif() + +######################################## Packaging ######################################## + +if(NOT LAUNCHER_FUZZ_ONLY AND UNIX AND NOT APPLE) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/LauncherPackaging.cmake) +endif() + + + +################################ Included Libs ################################ + +include(ExternalProject) +set_directory_properties(PROPERTIES EP_BASE External) + +include(useMinimalLibs) +projt_add_internallibs() + +############################### Built Artifacts ############################### + +if(NOT LAUNCHER_FUZZ_ONLY) + projt_push_output_dirs("buildconfig") + add_subdirectory(buildconfig) + projt_pop_output_dirs() +endif() + +if(BUILD_TESTING) + projt_push_output_dirs("tests") + add_subdirectory(tests) + projt_pop_output_dirs() +endif() +if(BUILD_FUZZERS) + projt_push_output_dirs("fuzz") + add_subdirectory(fuzz EXCLUDE_FROM_ALL) + projt_pop_output_dirs() +endif() +# NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. +if(NOT LAUNCHER_FUZZ_ONLY) + projt_push_output_dirs("launcher") + add_subdirectory(launcher) + projt_pop_output_dirs() +endif() + +# Ensure test executables pick up CEF runtime dependencies once they exist. +if(BUILD_TESTING AND TARGET projt_cef_runtime_deps) + get_property(_projt_launcher_test_targets GLOBAL PROPERTY PROJT_LAUNCHER_TEST_TARGETS) + foreach(_projt_launcher_test_target IN LISTS _projt_launcher_test_targets) + if(TARGET "${_projt_launcher_test_target}") + target_link_libraries(${_projt_launcher_test_target} projt_cef_runtime_deps) + endif() + endforeach() + unset(_projt_launcher_test_targets) + unset(_projt_launcher_test_target) +endif() diff --git a/archived/projt-launcher/CMakePresets.json b/archived/projt-launcher/CMakePresets.json new file mode 100644 index 0000000000..ff40004eaf --- /dev/null +++ b/archived/projt-launcher/CMakePresets.json @@ -0,0 +1,326 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "cmakeMinimumRequired": { + "major": 3, + "minor": 28 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "build", + "installDir": "install", + "generator": "Ninja Multi-Config", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", + "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}", + "Launcher_ENABLE_JAVA_DOWNLOADER": "ON", + "ENABLE_LTO": "ON" + } + }, + { + "name": "linux", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "cacheVariables": { + "LAUNCHER_USE_WEBENGINE": "OFF", + "LAUNCHER_USE_CEF": "OFF", + "LAUNCHER_DISABLE_HUB": "ON" + } + }, + { + "name": "linux-withHub", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "macos", + "displayName": "macOS", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", + "LAUNCHER_DISABLE_HUB": "ON", + "LAUNCHER_USE_WEBENGINE": "OFF" + } + }, + { + "name": "macos_universal-withHub", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64" + } + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "LAUNCHER_DISABLE_HUB": "ON", + "LAUNCHER_USE_WEBVIEW2": "OFF" + } + }, + { + "name": "windows_msvc-withHub", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + } + ], + "buildPresets": [ + { + "name": "linux", + "displayName": "Linux", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux" + }, + { + "name": "linux-withHub", + "displayName": "Linux", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux-withHub" + }, + { + "name": "macos", + "displayName": "macOS", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos" + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "configurePreset": "macos_universal" + }, + { + "name": "macos_universal-withHub", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "configurePreset": "macos_universal-withHub" + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_mingw" + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc" + }, + { + "name": "windows_msvc-withHub", + "displayName": "Windows (MSVC)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc" + } + ], + "testPresets": [ + { + "name": "base", + "hidden": true, + "output": { + "outputOnFailure": true, + "verbosity": "extra" + }, + "execution": { + "noTestsAction": "error" + }, + "filter": { + "exclude": { + "name": "^example64|example$" + } + } + }, + { + "name": "linux", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux" + }, + { + "name": "linux-withHub", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux-withHub" + }, + { + "name": "macos", + "displayName": "macOS", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos" + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos_universal" + }, + { + "name": "macos_universal-withHub", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos_universal-withHub" + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_mingw" + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc" + }, + { + "name": "windows_msvc-withHub", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc-withHub" + } + ] +} \ No newline at end of file diff --git a/archived/projt-launcher/CODE_OF_CONDUCT b/archived/projt-launcher/CODE_OF_CONDUCT new file mode 100644 index 0000000000..4935ed7b74 --- /dev/null +++ b/archived/projt-launcher/CODE_OF_CONDUCT @@ -0,0 +1,132 @@ +Project Tick Code of Conduct +============================ + +=*=*=*=*=*=*=*=*=*=*=*=*=*=* +|Version 2, 15 February 2026| +=*=*=*=*=*=*=*=*=*=*=*=*=*=* + +1. Purpose +---------- + +Project Tick is a free software infrastructure initiative committed to technical excellence, legal clarity, and long-term sustainability. + +This Code of Conduct defines the behavioral and ethical standards expected across all Project Tick spaces. Its purpose is to protect contributors, maintain the integrity of the ecosystem, and ensure the security and reliability of our infrastructure. + +Participation in Project Tick constitutes agreement to abide by this Code. + +2. Core Principles +------------------ + +Project Tick operates according to the following foundational principles: + +- Respect for human dignity +- Commitment to technical integrity +- Strict adherence to licensing and attribution requirements +- Protection of infrastructure security and supply-chain integrity +- Good-faith collaboration + +These principles apply equally to maintainers, contributors, and participants. + +3. Expected Conduct +------------------- + +All participants are expected to: + +1. Engage in respectful, constructive, and technically relevant discussion. +2. Accept review, feedback, and architectural decisions professionally. +3. Provide accurate attribution and comply fully with applicable licenses. +4. Disclose conflicts of interest where relevant. +5. Protect confidential or sensitive information. +6. Act in ways that strengthen the reliability and long-term sustainability of the ecosystem. +7. Respect differing technical or philosophical viewpoints without personal hostility. + +Criticism of ideas is encouraged. Personal attacks are not. + +4. Prohibited Conduct +--------------------- + +The following behaviors are considered violations of this Code of Conduct: + +4.1 Personal and Social Misconduct +********************************** + +- Harassment, intimidation, or threats +- Discriminatory behavior based on identity or personal characteristics +- Insulting, demeaning, or hostile personal attacks +- Sexualized behavior inappropriate to a professional technical environment +- Sharing private information without explicit consent + +4.2 Technical and Legal Misconduct +********************************** + +- Intentional submission of malicious code +- Supply-chain compromise attempts +- License violations or intentional misattribution +- Plagiarism or failure to credit sources +- False copyright claims +- Impersonation of maintainers or other participants +- Infrastructure abuse, including CI/CD exploitation or service disruption +- Repeated bad-faith technical disruption + +Project maintainers reserve the right to determine whether conduct violates the spirit or intent of this Code. + +5. Scope +-------- + +This Code of Conduct applies to: + +- All Project Tick repositories +- Mailing lists and communication channels +- Issue trackers and merge requests +- Continuous integration systems +- Infrastructure services and hosting environments +- Official Project Tick domains and email accounts +- Public representation of the project + +Individuals representing Project Tick in any official capacity are expected to uphold this Code at all times. + +6. Reporting +------------ + +Violations may be reported confidentially to: + +conduct@projecttick.org + +Reports will be reviewed in a timely and objective manner. Confidentiality will be respected to the extent reasonably possible. + +Malicious or knowingly false reports may themselves constitute a violation. + +7. Enforcement +-------------- + +Enforcement decisions are made by the Project Tick maintainers. + +Depending on severity, actions may include: + +1. Private warning +2. Temporary communication restriction +3. Temporary suspension of repository access +4. Permanent removal of access to Project Tick spaces + +Serious security, infrastructure, or legal violations may result in immediate permanent removal. + +Enforcement decisions are final and are not subject to public vote. + +8. Remediation +-------------- + +Where appropriate, resolution may involve: + +- Acknowledgment of harm +- Corrective action or documentation +- License compliance remediation +- Demonstrated behavioral improvement + +Reinstatement after suspension is at the sole discretion of the maintainers. + +9. Independence +--------------- + +This document was inspired by Contributor Covenant 3.0 but has been independently written and adapted to reflect the governance, infrastructure, and legal priorities of Project Tick. + +Project Tick maintains full authority over the interpretation and enforcement of this Code. diff --git a/archived/projt-launcher/COPYING.md b/archived/projt-launcher/COPYING.md new file mode 100644 index 0000000000..9574798764 --- /dev/null +++ b/archived/projt-launcher/COPYING.md @@ -0,0 +1,345 @@ +ProjT Launcher +============== + + ProjT Launcher - Minecraft Launcher + Copyright (C) 2026 Project Tick + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + This file incorporates work covered by the following copyright and + permission notice: + + This project includes a modified version of the Prism Launcher logo. + + Original logo: + Prism Launcher Logo + © Prism Launcher Contributors + Licensed under CC BY-SA 4.0 + + Modified version: + ProjT Launcher Logo + © 2026 Project Tick + Licensed under CC BY-SA 4.0 + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Prism Launcher +============== + + Prism Launcher - Minecraft Launcher + Copyright (C) 2022-2025 Prism Launcher Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + This file incorporates work covered by the following copyright and + permission notice: + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +PolyMC +====== + + PolyMC - Minecraft Launcher + Copyright (C) 2021-2022 PolyMC Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + This file incorporates work covered by the following copyright and + permission notice: + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +MultiMC +======= + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +program_info (`program_info/`) +============================== + +Logos and branding assets in this directory are licensed under +Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0). + +But instance_icons.svg is licensed under Creative Commons Attribution-ShareAlike 4.0 International +(CC BY-SA 4.0). + +This license applies only to branding assets and does not affect +the licensing of the ProjT Launcher source code. + +See: +- program_info/LICENSE.projecttick +- program_info/LICENSE.instanceicons + +MinGW-w64 runtime (Windows) +=========================== + + Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project + + This license has been certified as open source. It has also been designated + as GPL compatible by the Free Software Foundation (FSF). + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions in source code must retain the accompanying copyright + notice, this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the accompanying + copyright notice, this list of conditions, and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + 3. Names of the copyright holders must not be used to endorse or promote + products derived from this software without prior written permission + from the copyright holders. + 4. The right to distribute this software or to use it for any purpose does + not give you the right to use Servicemarks (sm) or Trademarks (tm) of + the copyright holders. Use of them is covered by separate agreement + with the copyright holders. + 5. If any files are modified, you must cause the modified files to carry + prominent notices stating that you changed the files and the date of + any change. + + Disclaimer + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. + +rainbow (KGuiAddons) +==================== + + Copyright (C) 2007 Matthew Woehlke + Copyright (C) 2007 Olaf Schmidt + Copyright (C) 2007 Thomas Zander + Copyright (C) 2007 Zack Rusin + Copyright (C) 2015 Petr Mrazek + Copyright (C) 2026 Project Tick + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + +Batch icon set +============== + + You are free to use Batch (the "icon set") or any part thereof (the "icons") + in any personal, open-source or commercial work without obligation of payment + (monetary or otherwise) or attribution. Do not sell the icon set, host + the icon set or rent the icon set (either in existing or modified form). + + While attribution is optional, it is always appreciated. + + Intellectual property rights are not transferred with the download of the icons. + + EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL ADAM WHITCROFT + BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, + PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS, + EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +Material Design Icons +===================== + + Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/), + with Reserved Font Name Material Design Icons. + Copyright (c) 2014, Google (http://www.google.com/design/) + uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE + + This Font Software is licensed under the SIL Open Font License, Version 1.1. + This license is copied below, and is also available with a FAQ at: + http://scripts.sil.org/OFL + +lionshead +========= + + Code has been taken from https://github.com/natefoo/lionshead and loosely + translated to C++ laced with Qt. + + MIT License + + Copyright (c) 2017 Nate Coraor + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Breeze icons +============ + + Copyright (C) 2014 Uri Herrera and others + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . + +Oxygen Icons +============ + + The Oxygen Icon Theme + Copyright (C) 2007 Nuno Pinheiro + Copyright (C) 2007 David Vignoni + Copyright (C) 2007 David Miller + Copyright (C) 2007 Johann Ollivier Lapeyre + Copyright (C) 2007 Kenneth Wimer + Copyright (C) 2007 Riccardo Iaconelli + + and others + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . + +vcpkg (`cmake/vcpkg-ports`) +=========================== + + MIT License + + Copyright (c) Microsoft Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this + software and associated documentation files (the "Software"), to deal in the Software + without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be included in all copies + or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/archived/projt-launcher/Containerfile b/archived/projt-launcher/Containerfile new file mode 100644 index 0000000000..59595fe55b --- /dev/null +++ b/archived/projt-launcher/Containerfile @@ -0,0 +1,74 @@ +ARG DEBIAN_VERSION=stable-slim + +FROM docker.io/library/debian:${DEBIAN_VERSION} + +ARG QT_ABI=gcc_64 +ARG QT_ARCH= +ARG QT_HOST=linux +ARG QT_TARGET=desktop +ARG QT_VERSION=6.10.2 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get --assume-yes upgrade \ + && apt-get --assume-yes autopurge + +# Use Adoptium for Java 17 +RUN apt-get --assume-yes --no-install-recommends install \ + apt-transport-https ca-certificates curl gpg +RUN curl -L https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg +RUN echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list +RUN apt-get update + +# Install base dependencies +RUN apt-get --assume-yes --no-install-recommends install \ + # Compilers + clang lld llvm temurin-17-jdk \ + # Build system + cmake ninja-build extra-cmake-modules pkg-config \ + # Dependencies + cmark gamemode-dev libarchive-dev libcmark-dev libgamemode0 libgl1-mesa-dev libqrencode-dev libtomlplusplus-dev scdoc zlib1g-dev \ + # Tooling + clang-format clang-tidy git + +# Use LLD by default for faster linking +ENV CMAKE_LINKER_TYPE=lld + +# Prepare and install Qt +## Setup UTF-8 locale (required, apparently) +RUN apt-get --assume-yes --no-install-recommends install locales +RUN echo "C.UTF-8 UTF-8" > /etc/locale.gen +RUN locale-gen +ENV LC_ALL=C.UTF-8 + +## Some libraries are required for the official binaries +RUN apt-get --assume-yes --no-install-recommends install \ + libglib2.0-0t64 libxkbcommon0 python3-pip + +RUN pip3 install --break-system-packages aqtinstall +RUN aqt install-qt \ + ${QT_HOST} ${QT_TARGET} ${QT_VERSION} ${QT_ARCH} \ + --outputdir /opt/qt \ + --modules qtimageformats qtnetworkauth + +ENV PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/bin:$PATH +ENV QT_PLUGIN_PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/plugins/ + +## We don't use these. Nuke them +RUN rm -rf \ + "$QT_PLUGIN_PATH"/designer \ + "$QT_PLUGIN_PATH"/help \ + # "$QT_PLUGIN_PATH"/platformthemes/libqgtk3.so \ + "$QT_PLUGIN_PATH"/printsupport \ + "$QT_PLUGIN_PATH"/qmllint \ + "$QT_PLUGIN_PATH"/qmlls \ + "$QT_PLUGIN_PATH"/qmltooling \ + "$QT_PLUGIN_PATH"/sqldrivers + +# Setup workspace +RUN mkdir /work +WORKDIR /work + +ENTRYPOINT ["bash"] +CMD ["-i"] diff --git a/archived/projt-launcher/MAINTAINERS b/archived/projt-launcher/MAINTAINERS new file mode 100644 index 0000000000..a109c34e2e --- /dev/null +++ b/archived/projt-launcher/MAINTAINERS @@ -0,0 +1,19 @@ +# MAINTAINERS +# +# One block per maintainer. +# Edit the sample block below and add new blocks as needed. +# +# Fields: +# - Name: Display name +# - GitHub: GitHub handle (with @) +# - Email: Primary contact email +# - Paths: Comma-separated glob patterns (repo-relative) +# +# Matching notes: +# - Use ** for recursive directory matching. +# - This file is used as the ownership source for maintainer email mapping. + +[Mehmet Samet Duman] +GitHub: @YongDo-Hyun +Email: yongdohyun@mail.projecttick.org +Paths: ** diff --git a/archived/projt-launcher/README b/archived/projt-launcher/README new file mode 100644 index 0000000000..2380efb710 --- /dev/null +++ b/archived/projt-launcher/README @@ -0,0 +1,125 @@ +ProjT Launcher +============== + +A Minecraft launcher engineered for long-term maintainability, architectural clarity, +and controlled ecosystem evolution. + +ProjT Launcher is a structurally disciplined fork of Prism Launcher, diverging +intentionally to prevent maintenance decay, dependency drift, and architectural +erosion over time. + +Why ProjT Launcher? +------------------- + +Long-term maintainability + Explicit architectural constraints and review rules prevent uncontrolled technical debt. + +Controlled third-party integration + External dependencies are maintained as detached forks with documented patch and update policies. + +Deterministic CI and builds + Exact dependency versions and constrained build inputs enable reproducible builds across environments. + +Structural clarity + Enforced MVVM boundaries and clearly separated modules simplify review, refactoring, and long-term contribution. + +Download +-------- + +Releases: https://gitlab.com/Project-Tick/core/ProjT-Launcher/-/releases – Stable builds only. Nightly builds are not provided. +Website: https://projecttick.org/p/projt-launcher/ + +Build +----- + +Quick start for release builds: + + +git clone --recursive https://gitlab.com/Project-Tick/core/ProjT-Launcher.git +cd ProjT-Launcher +cmake --preset [macos OR linux OR windows_msvc OR windows_mingw] +cmake --build --preset [macos OR linux OR windows_msvc OR windows_mingw] --config [Debug OR Release] + + +For development setup with presets and full tooling, see docs/contributing/GETTING_STARTED.md + +Requirements +************ + +| Tool | Version | +| -------- | ------- | +| CMake | 3.22+ | +| Qt | 6.10.x | +| Compiler | C++20 | + +Nix +*** + +nix build .#projtlauncher + + +Structure +--------- + +launcher/ Application (C++/Qt) +website/ Website (Eleventy) +bot/ Automation (Cloudflare Workers) +meta/ Metadata generator (Python) +docs/ Documentation + +Detached Fork Libraries +*********************** + +zlib/ Compression +bzip2/ Compression +quazip/ ZIP handling +cmark/ Markdown parsing +tomlplusplus/ TOML parsing +libqrencode/ QR codes +libnbtplusplus/ NBT format +gamemode/ GameMode + +NOTE: +These directories contain original upstream READMEs preserved for reference. +For Project Tick–specific documentation, see docs/handbook/ or https://projecttick.org/handbook + +Vendored Libraries +****************** + +LocalPeer/ Single instance +murmur2/ Hash functions +qdcss/ Dark CSS +rainbow/ Terminal colors +systeminfo/ System info + +Documentation +------------- + +Contributing: CONTRIBUTING.md +Getting Started: docs/contributing/GETTING_STARTED.md +Code Style: docs/contributing/CODE_STYLE.md +Architecture: docs/contributing/ARCHITECTURE.md +Developer Handbook: docs/handbook/ or https://projecttick.org/handbook + +License +------- + +Multiple licenses apply to different components: + +- Launcher: GPL-3.0-only: LICENSES/GPL-3.0-only.txt and Please see code headers +- Metadata: MS-PL: meta/LICENSE + +Contributions to each component are licensed under its respective license. See COPYING for details. + +Links +----- + +Website: https://projecttick.org/p/projt-launcher/ +Project Tick Website: https://projecttick.org/ +Issues: https://gitlab.com/Project-Tick/core/ProjT-Launcher/-/issues + +-------------------------------------------------------------- + +This project is Official Project Tick Product. + +Please see Project Tick Trademark of https://projecttick.org/trademark diff --git a/archived/projt-launcher/bootstrap/macos/Bootstrap.m b/archived/projt-launcher/bootstrap/macos/Bootstrap.m new file mode 100644 index 0000000000..188b165a78 --- /dev/null +++ b/archived/projt-launcher/bootstrap/macos/Bootstrap.m @@ -0,0 +1,423 @@ +#import +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define APP_NAME "ProjT Launcher.app" +#define INSTALL_PATH "/Applications/" APP_NAME +#define FEED_URL BOOTSTRAP_FEED_URL +#define URL_TEMPLATE BOOTSTRAP_URL_TEMPLATE + +extern char **environ; + +static NSWindow *g_window = nil; +static NSTextField *g_statusLabel = nil; +static NSProgressIndicator *g_progress = nil; + +static void pump_events(void) { + @autoreleasepool { + NSEvent *event = nil; + while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:[NSDate dateWithTimeIntervalSinceNow:0] + inMode:NSDefaultRunLoopMode + dequeue:YES])) { + [NSApp sendEvent:event]; + } + [NSApp updateWindows]; + } +} + +static void init_ui(void) { + @autoreleasepool { + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + NSRect frame = NSMakeRect(0, 0, 520, 280); + g_window = [[NSWindow alloc] initWithContentRect:frame + styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable) + backing:NSBackingStoreBuffered + defer:NO]; + [g_window setTitle:@"ProjT Launcher Installer"]; + [g_window center]; + + NSView *content = [g_window contentView]; + + NSTextField *title = [[NSTextField alloc] initWithFrame:NSMakeRect(24, 210, 472, 40)]; + [title setStringValue:@"ProjT Launcher Installer"]; + [title setBezeled:NO]; + [title setDrawsBackground:NO]; + [title setEditable:NO]; + [title setSelectable:NO]; + [title setFont:[NSFont systemFontOfSize:18 weight:NSFontWeightSemibold]]; + [content addSubview:title]; + + g_statusLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(24, 165, 472, 30)]; + [g_statusLabel setStringValue:@"Preparing..."]; + [g_statusLabel setBezeled:NO]; + [g_statusLabel setDrawsBackground:NO]; + [g_statusLabel setEditable:NO]; + [g_statusLabel setSelectable:NO]; + [g_statusLabel setFont:[NSFont systemFontOfSize:13 weight:NSFontWeightRegular]]; + [content addSubview:g_statusLabel]; + + g_progress = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(24, 125, 472, 12)]; + [g_progress setIndeterminate:YES]; + [g_progress setStyle:NSProgressIndicatorStyleBar]; + [g_progress startAnimation:nil]; + [content addSubview:g_progress]; + + [g_window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + pump_events(); + } +} + +static void update_status(const char *message) { + if (!g_statusLabel || !message) return; + @autoreleasepool { + NSString *text = [NSString stringWithUTF8String:message]; + [g_statusLabel setStringValue:text ?: @""]; + pump_events(); + } +} + +static void show_alert(const char *title, const char *message) { + @autoreleasepool { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:(title ? [NSString stringWithUTF8String:title] : @"ProjT Launcher Installer")]; + [alert setInformativeText:(message ? [NSString stringWithUTF8String:message] : @"")]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; + } +} + +static int run_cmd(char *const argv[]) { + pid_t pid = 0; + int status = 0; + if (posix_spawnp(&pid, argv[0], NULL, NULL, argv, environ) != 0) { + return -1; + } + if (waitpid(pid, &status, 0) < 0) { + return -1; + } + if (WIFEXITED(status)) return WEXITSTATUS(status); + return -1; +} + +static char *strdup_safe(const char *s) { + if (!s) return NULL; + size_t len = strlen(s); + char *out = (char *)malloc(len + 1); + if (!out) return NULL; + memcpy(out, s, len + 1); + return out; +} + +static char *extract_version(const char *text) { + if (!text) return NULL; + const char *p = text; + while (*p && (*p < '0' || *p > '9')) p++; + if (!*p) return NULL; + const char *start = p; + while (*p) { + if ((*p >= '0' && *p <= '9') || *p == '.' || *p == '-' ) { + p++; + continue; + } + break; + } + size_t len = (size_t)(p - start); + if (len == 0) return NULL; + char *out = (char *)malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +static int version_compare(const char *a, const char *b) { + if (!a && !b) return 0; + if (!a) return -1; + if (!b) return 1; + char *ca = strdup_safe(a); + char *cb = strdup_safe(b); + if (!ca || !cb) { + free(ca); + free(cb); + return 0; + } + const char *delim = "._-"; + char *sa = strtok(ca, delim); + char *sb = strtok(cb, delim); + while (sa || sb) { + long va = sa ? strtol(sa, NULL, 10) : 0; + long vb = sb ? strtol(sb, NULL, 10) : 0; + if (va < vb) { free(ca); free(cb); return -1; } + if (va > vb) { free(ca); free(cb); return 1; } + sa = sa ? strtok(NULL, delim) : NULL; + sb = sb ? strtok(NULL, delim) : NULL; + } + free(ca); + free(cb); + return 0; +} + +static char *get_installed_version(void) { + CFURLRef appURL = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, + (const UInt8 *)INSTALL_PATH, + strlen(INSTALL_PATH), + true); + if (!appURL) return NULL; + CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, appURL); + CFRelease(appURL); + if (!bundle) return NULL; + CFStringRef version = CFBundleGetValueForInfoDictionaryKey(bundle, CFSTR("CFBundleShortVersionString")); + if (!version) { + CFRelease(bundle); + return NULL; + } + char buf[128]; + if (!CFStringGetCString(version, buf, sizeof(buf), kCFStringEncodingUTF8)) { + CFRelease(bundle); + return NULL; + } + CFRelease(bundle); + return strdup_safe(buf); +} + +typedef struct { + char *version; +} ReleaseInfo; + +static bool attr_matches(xmlAttr *attr, const char *prefix, const char *name, const char *value) { + if (!attr || !attr->name) return false; + if (strcmp((const char *)attr->name, name) != 0) return false; + if (prefix) { + if (!attr->ns || !attr->ns->prefix) return false; + if (strcmp((const char *)attr->ns->prefix, prefix) != 0) return false; + } + xmlChar *val = xmlNodeListGetString(attr->doc, attr->children, 1); + bool ok = val && strcmp((const char *)val, value) == 0; + if (val) xmlFree(val); + return ok; +} + +static xmlChar *get_child_content(xmlNode *node, const char *prefix, const char *name) { + for (xmlNode *c = node->children; c; c = c->next) { + if (c->type != XML_ELEMENT_NODE || !c->name) continue; + if (strcmp((const char *)c->name, name) != 0) continue; + if (prefix) { + if (!c->ns || !c->ns->prefix) continue; + if (strcmp((const char *)c->ns->prefix, prefix) != 0) continue; + } + return xmlNodeGetContent(c); + } + return NULL; +} + +static ReleaseInfo parse_feed(const char *path) { + ReleaseInfo info = {0}; + xmlDoc *doc = xmlReadFile(path, NULL, XML_PARSE_NOERROR | XML_PARSE_NOWARNING); + if (!doc) return info; + xmlNode *root = xmlDocGetRootElement(doc); + if (!root) { + xmlFreeDoc(doc); + return info; + } + for (xmlNode *channel = root->children; channel; channel = channel->next) { + if (channel->type != XML_ELEMENT_NODE) continue; + for (xmlNode *item = channel->children; item; item = item->next) { + if (item->type != XML_ELEMENT_NODE) continue; + if (strcmp((const char *)item->name, "item") != 0) continue; + + xmlChar *ver = get_child_content(item, "sparkle", "shortVersionString"); + if (!ver) ver = get_child_content(item, "sparkle", "version"); + if (!ver) { + xmlChar *title = get_child_content(item, NULL, "title"); + if (title) { + char *from_title = extract_version((const char *)title); + if (from_title) { + info.version = from_title; + } + xmlFree(title); + } + } + + if (ver && !info.version) { + info.version = strdup_safe((const char *)ver); + xmlFree(ver); + break; + } + if (ver) xmlFree(ver); + if (info.version) break; + } + if (info.version) break; + } + xmlFreeDoc(doc); + return info; +} + +static int download_file(const char *url, const char *out_path) { + char *argv[] = {"/usr/bin/curl", "-L", "--fail", "--silent", "--show-error", "-o", (char *)out_path, (char *)url, NULL}; + return run_cmd(argv); +} + +static int unzip_to(const char *zip_path, const char *dest_dir) { + char *argv[] = {"/usr/bin/ditto", "-x", "-k", (char *)zip_path, (char *)dest_dir, NULL}; + return run_cmd(argv); +} + +static int verify_codesign(const char *app_path) { + char *argv1[] = {"/usr/bin/codesign", "--verify", "--deep", "--strict", "--verbose=2", (char *)app_path, NULL}; + char *argv2[] = {"/usr/sbin/spctl", "--assess", "--type", "execute", "--verbose=2", (char *)app_path, NULL}; + if (run_cmd(argv1) != 0) return -1; + if (run_cmd(argv2) != 0) { + if (getenv("PROJT_BOOTSTRAP_STRICT")) return -1; + fprintf(stderr, "warning: spctl assessment failed; continuing (set PROJT_BOOTSTRAP_STRICT=1 to enforce).\n"); + } + return 0; +} + +static int install_app(const char *src_app) { + if (access(INSTALL_PATH, F_OK) == 0) { + char *rm_argv[] = {"/bin/rm", "-rf", INSTALL_PATH, NULL}; + if (run_cmd(rm_argv) != 0) { + // best effort; continue + } + } + + char *ditto_argv[] = {"/usr/bin/ditto", "--rsrc", (char *)src_app, INSTALL_PATH, NULL}; + if (run_cmd(ditto_argv) == 0) return 0; + + // Try with admin privileges + char cmd[2048]; + snprintf(cmd, sizeof(cmd), + "do shell script \"rm -rf '%s'; /usr/bin/ditto --rsrc '%s' '%s'\" with administrator privileges", + INSTALL_PATH, src_app, INSTALL_PATH); + char *osa_argv[] = {"/usr/bin/osascript", "-e", cmd, NULL}; + return run_cmd(osa_argv); +} + +static int launch_app(void) { + char *argv[] = {"/usr/bin/open", "-a", "ProjT Launcher", NULL}; + return run_cmd(argv); +} + +int main(void) { + init_ui(); + update_status("Checking for updates..."); + + const char *feed_url = FEED_URL; + if (!feed_url || strlen(feed_url) == 0) { + show_alert("ProjT Launcher Installer", "Feed URL is not configured."); + return 1; + } + + char tmpdir[] = "/tmp/projt-bootstrap-XXXXXX"; + if (!mkdtemp(tmpdir)) { + show_alert("ProjT Launcher Installer", "Failed to create temp directory."); + return 1; + } + + char feed_path[1024]; + snprintf(feed_path, sizeof(feed_path), "%s/feed.xml", tmpdir); + update_status("Downloading update feed..."); + if (download_file(feed_url, feed_path) != 0) { + show_alert("ProjT Launcher Installer", "Failed to download update feed."); + return 1; + } + + ReleaseInfo info = parse_feed(feed_path); + if (!info.version) { + show_alert("ProjT Launcher Installer", "Failed to parse update feed."); + return 1; + } + + char *download_url = NULL; + if (URL_TEMPLATE && strlen(URL_TEMPLATE) > 0) { + size_t needed = snprintf(NULL, 0, URL_TEMPLATE, info.version, info.version); + download_url = (char *)malloc(needed + 1); + if (!download_url) { + show_alert("ProjT Launcher Installer", "Out of memory while building URL."); + free(info.version); + return 1; + } + snprintf(download_url, needed + 1, URL_TEMPLATE, info.version, info.version); + } else { + show_alert("ProjT Launcher Installer", "Download URL template is not configured."); + free(info.version); + return 1; + } + + char *installed = get_installed_version(); + if (installed && version_compare(installed, info.version) >= 0) { + update_status("Launching ProjT Launcher..."); + launch_app(); + free(installed); + free(info.version); + free(download_url); + return 0; + } + free(installed); + + char zip_path[1024]; + snprintf(zip_path, sizeof(zip_path), "%s/launcher.zip", tmpdir); + update_status("Downloading latest version..."); + if (download_file(download_url, zip_path) != 0) { + show_alert("ProjT Launcher Installer", "Failed to download installer zip."); + free(info.version); + free(download_url); + return 1; + } + + char unpack_dir[1024]; + snprintf(unpack_dir, sizeof(unpack_dir), "%s/unpack", tmpdir); + mkdir(unpack_dir, 0755); + update_status("Extracting..."); + if (unzip_to(zip_path, unpack_dir) != 0) { + show_alert("ProjT Launcher Installer", "Failed to extract installer zip."); + free(info.version); + free(download_url); + return 1; + } + + char app_path[1024]; + snprintf(app_path, sizeof(app_path), "%s/%s", unpack_dir, APP_NAME); + if (access(app_path, F_OK) != 0) { + show_alert("ProjT Launcher Installer", "Downloaded archive did not contain the app bundle."); + free(info.version); + free(download_url); + return 1; + } + + update_status("Installing to /Applications..."); + if (install_app(app_path) != 0) { + show_alert("ProjT Launcher Installer", "Failed to install into /Applications."); + free(info.version); + free(download_url); + return 1; + } + + update_status("Verifying signature..."); + if (verify_codesign(INSTALL_PATH) != 0) { + show_alert("ProjT Launcher Installer", "Installed app failed signature verification."); + free(info.version); + free(download_url); + return 1; + } + + update_status("Launching ProjT Launcher..."); + launch_app(); + free(info.version); + free(download_url); + return 0; +} diff --git a/archived/projt-launcher/bootstrap/macos/CMakeLists.txt b/archived/projt-launcher/bootstrap/macos/CMakeLists.txt new file mode 100644 index 0000000000..28f496ab5b --- /dev/null +++ b/archived/projt-launcher/bootstrap/macos/CMakeLists.txt @@ -0,0 +1,38 @@ +find_package(LibXml2 REQUIRED) + +set(BOOTSTRAP_NAME "ProjT Launcher Installer") +set(BOOTSTRAP_ICNS "${CMAKE_SOURCE_DIR}/${Launcher_Branding_ICNS}") +get_filename_component(BOOTSTRAP_ICNS_NAME "${BOOTSTRAP_ICNS}" NAME) +set(BOOTSTRAP_BUNDLE_ID "org.projecttick.ProjTLauncherInstaller") + +add_executable(ProjTLauncherBootstrap MACOSX_BUNDLE + Bootstrap.m +) + +target_include_directories(ProjTLauncherBootstrap PRIVATE ${LIBXML2_INCLUDE_DIR}) +target_link_libraries(ProjTLauncherBootstrap PRIVATE ${LIBXML2_LIBRARIES}) +target_link_libraries(ProjTLauncherBootstrap PRIVATE "-framework CoreFoundation") +target_link_libraries(ProjTLauncherBootstrap PRIVATE "-framework Cocoa") + +target_compile_definitions(ProjTLauncherBootstrap PRIVATE + BOOTSTRAP_FEED_URL="${MACOSX_BOOTSTRAP_FEED_URL}" + BOOTSTRAP_URL_TEMPLATE="${MACOSX_BOOTSTRAP_URL_TEMPLATE}" +) + +set_target_properties(ProjTLauncherBootstrap PROPERTIES + OUTPUT_NAME "${BOOTSTRAP_NAME}" + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in" + MACOSX_BUNDLE_BUNDLE_NAME "${BOOTSTRAP_NAME}" + MACOSX_BUNDLE_GUI_IDENTIFIER "${BOOTSTRAP_BUNDLE_ID}" + MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_NAME}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}" + MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}" + MACOSX_BUNDLE_COPYRIGHT "${Launcher_Copyright_Mac}" + MACOSX_BUNDLE_INFO_STRING "${MACOSX_BUNDLE_INFO_STRING}" + MACOSX_BUNDLE_ICON_FILE "${BOOTSTRAP_ICNS_NAME}" +) + +target_sources(ProjTLauncherBootstrap PRIVATE "${BOOTSTRAP_ICNS}") +set_source_files_properties("${BOOTSTRAP_ICNS}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + +install(TARGETS ProjTLauncherBootstrap BUNDLE DESTINATION "." COMPONENT launcher) diff --git a/archived/projt-launcher/bootstrap/macos/Info.plist.in b/archived/projt-launcher/bootstrap/macos/Info.plist.in new file mode 100644 index 0000000000..eb131339c0 --- /dev/null +++ b/archived/projt-launcher/bootstrap/macos/Info.plist.in @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + LSMinimumSystemVersion + 12.0 + NSHighResolutionCapable + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + LSBackgroundOnly + + + diff --git a/archived/projt-launcher/buildconfig/BuildConfig.cpp.in b/archived/projt-launcher/buildconfig/BuildConfig.cpp.in new file mode 100644 index 0000000000..465641fa13 --- /dev/null +++ b/archived/projt-launcher/buildconfig/BuildConfig.cpp.in @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * // SPDX-License-Identifier: GPL-3.0-only + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include +#include +#include "BuildConfig.h" + +const Config BuildConfig; + +Config::Config() +{ + // Name and copyright + LAUNCHER_NAME = "@Launcher_Name@"; + LAUNCHER_APP_BINARY_NAME = "@Launcher_AppBinaryName@"; + LAUNCHER_DISPLAYNAME = "@Launcher_DisplayName@"; + LAUNCHER_COPYRIGHT = "@Launcher_Copyright@"; + LAUNCHER_DOMAIN = "@Launcher_Domain@"; + LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@"; + LAUNCHER_GIT = "@Launcher_Git@"; + LAUNCHER_APPID = "@Launcher_AppID@"; + LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; + + USER_AGENT = "@Launcher_UserAgent@"; + + // Version information + VERSION_MAJOR = @Launcher_VERSION_MAJOR@; + VERSION_MINOR = @Launcher_VERSION_MINOR@; + VERSION_PATCH = @Launcher_VERSION_PATCH@; + VERSION_TWEAK = @Launcher_VERSION_TWEAK@; + + BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; + BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; + BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@"; + UPDATER_RELEASES_URL = "@Launcher_UPDATER_RELEASES_URL@"; + + COMPILER_NAME = "@Launcher_COMPILER_NAME@"; + COMPILER_VERSION = "@Launcher_COMPILER_VERSION@"; + + COMPILER_TARGET_SYSTEM = "@Launcher_COMPILER_TARGET_SYSTEM@"; + COMPILER_TARGET_SYSTEM_VERSION = "@Launcher_COMPILER_TARGET_SYSTEM_VERSION@"; + COMPILER_TARGET_SYSTEM_PROCESSOR = "@Launcher_COMPILER_TARGET_PROCESSOR@"; + + MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; + MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; + + if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { + UPDATER_ENABLED = true; + } else if (!UPDATER_RELEASES_URL.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { + UPDATER_ENABLED = true; + } + +#cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER + JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; + + GIT_COMMIT = "@Launcher_GIT_COMMIT@"; + GIT_TAG = "@Launcher_GIT_TAG@"; + GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; + + // Assume that builds outside of Git repos are "stable" + if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") || + GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { + GIT_REFSPEC = "refs/heads/stable"; + GIT_TAG = versionString(); + GIT_COMMIT = ""; + } + + if (GIT_REFSPEC.startsWith("refs/heads/")) { + VERSION_CHANNEL = GIT_REFSPEC; + VERSION_CHANNEL.remove("refs/heads/"); + } else if (!GIT_COMMIT.isEmpty()) { + VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); + } else { + VERSION_CHANNEL = "unknown"; + } + + NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; + NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; + WIKI_URL = "@Launcher_WIKI_URL@"; + HELP_URL = "@Launcher_HELP_URL@"; + HUB_HOME_URL = "@Launcher_HUB_HOME_URL@"; + HUB_COMMUNITY_URL = "@Launcher_HUB_COMMUNITY_URL@"; + HUB_SEARCH_URL = "@Launcher_HUB_SEARCH_URL@"; + LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@"; + IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; + MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; + FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; + META_URL = "@Launcher_META_URL@"; + FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@"; + + GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; + OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; + + BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; + TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; + TRANSLATION_FILES_URL = "@Launcher_TRANSLATION_FILES_URL@"; + MATRIX_URL = "@Launcher_MATRIX_URL@"; + DISCORD_URL = "@Launcher_DISCORD_URL@"; + SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; +} + +QString Config::versionString() const +{ + return QString("%1.%2.%3-%4").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH).arg(VERSION_TWEAK);; +} + +QString Config::printableVersionString() const +{ + QString vstr = versionString(); + + // If the build is not a main release, append the channel + if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) { + vstr += "-" + VERSION_CHANNEL; + } + return vstr; +} + +QString Config::compilerID() const +{ + if (COMPILER_VERSION.isEmpty()) + return COMPILER_NAME; + return QStringLiteral("%1 - %2").arg(COMPILER_NAME).arg(COMPILER_VERSION); +} + +QString Config::systemID() const +{ + return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); +} diff --git a/archived/projt-launcher/buildconfig/BuildConfig.h b/archived/projt-launcher/buildconfig/BuildConfig.h new file mode 100644 index 0000000000..9adc5a73cc --- /dev/null +++ b/archived/projt-launcher/buildconfig/BuildConfig.h @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * // SPDX-License-Identifier: GPL-3.0-only + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ +#pragma once +#include +#include + +/** + * \brief The Config class holds all the build-time information passed from the build system. + */ +class Config +{ + public: + Config(); + QString LAUNCHER_NAME; + QString LAUNCHER_APP_BINARY_NAME; + QString LAUNCHER_DISPLAYNAME; + QString LAUNCHER_COPYRIGHT; + QString LAUNCHER_DOMAIN; + QString LAUNCHER_CONFIGFILE; + QString LAUNCHER_GIT; + QString LAUNCHER_APPID; + QString LAUNCHER_SVGFILENAME; + + /// The major version number. + int VERSION_MAJOR; + /// The minor version number. + int VERSION_MINOR; + /// The patch version number. + int VERSION_PATCH; + /// The tweak version number. + int VERSION_TWEAK; + + /** + * The version channel + * This is used by the updater to determine what channel the current version came from. + */ + QString VERSION_CHANNEL; + + bool UPDATER_ENABLED = false; + bool JAVA_DOWNLOADER_ENABLED = false; + + /// A short string identifying this build's platform or distribution. + QString BUILD_PLATFORM; + + /// A short string identifying this build's valid artifacts int he updater. For example, "lin64" or "win32". + QString BUILD_ARTIFACT; + + /// A string containing the build timestamp + QString BUILD_DATE; + + /// A string identifying the compiler use to build + QString COMPILER_NAME; + + /// A string identifying the compiler version used to build + QString COMPILER_VERSION; + + /// A string identifying the compiler target system os + QString COMPILER_TARGET_SYSTEM; + + /// A String identifying the compiler target system version + QString COMPILER_TARGET_SYSTEM_VERSION; + + /// A String identifying the compiler target processor + QString COMPILER_TARGET_SYSTEM_PROCESSOR; + + /// URL for the updater's release feed + QString UPDATER_RELEASES_URL; + + /// The public key used to sign releases for the Sparkle updater appcast + QString MAC_SPARKLE_PUB_KEY; + + /// URL for the Sparkle updater's appcast + QString MAC_SPARKLE_APPCAST_URL; + + /// User-Agent to use. + QString USER_AGENT; + + /// The git commit hash of this build + QString GIT_COMMIT; + + /// The git tag of this build + QString GIT_TAG; + + /// The git refspec of this build + QString GIT_REFSPEC; + + /** + * This is used to fetch the news RSS feed. + * It defaults in CMakeLists.txt to "https://multimc.org/rss.xml" + */ + QString NEWS_RSS_URL; + + /** + * URL that gets opened when the user clicks "More News" + */ + QString NEWS_OPEN_URL; + + /** + * URL that gets opened when the user clicks 'Launcher Help' + */ + QString WIKI_URL; + + /** + * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window + */ + QString HELP_URL; + + /** + * Launcher Hub home URL. + */ + QString HUB_HOME_URL; + + /** + * Launcher Hub community URL (e.g. Discord/Matrix landing page). + */ + QString HUB_COMMUNITY_URL; + + /** + * Launcher Hub search URL (with %1 placeholder). + */ + QString HUB_SEARCH_URL; + + /** + * URL that gets opened when the user succesfully logins. + */ + QString LOGIN_CALLBACK_URL; + + /** + * Client ID you can get from Imgur when you register an application + */ + QString IMGUR_CLIENT_ID; + + /** + * Client ID you can get from Microsoft Identity Platform when you register an application + */ + QString MSA_CLIENT_ID; + + /** + * Client API key for CurseForge + */ + QString FLAME_API_KEY; + + /** + * Metadata repository URL prefix + */ + QString META_URL; + + QString GLFW_LIBRARY_NAME; + QString OPENAL_LIBRARY_NAME; + + QString BUG_TRACKER_URL; + QString TRANSLATIONS_URL; + QString MATRIX_URL; + QString DISCORD_URL; + QString SUBREDDIT_URL; + + QString DEFAULT_RESOURCE_BASE = "https://resources.download.minecraft.net/"; + QString LIBRARY_BASE = "https://libraries.minecraft.net/"; + QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; + QString FMLLIBS_BASE_URL; + QString TRANSLATION_FILES_URL; + + QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; + + QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; + + QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + QString ATL_API_BASE_URL = "https://api.atlauncher.com/v1/"; + + QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; + /** + * The build that is reported to the Technic API. + */ + QString TECHNIC_API_BUILD = "multimc"; + + QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; + QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; + QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" }; + + QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; + + QString versionString() const; + /** + * \brief Converts the Version to a string. + * \return The version number in string format (major.minor.revision.build). + */ + QString printableVersionString() const; + + /** + * \brief Compiler ID String + * \return a string of the form "Name - Version" of just "Name" if the version is empty + */ + QString compilerID() const; + + /** + * \brief System ID String + * \return a string of the form "OS Verison Processor" + */ + QString systemID() const; +}; + +extern const Config BuildConfig; diff --git a/archived/projt-launcher/buildconfig/CMakeLists.txt b/archived/projt-launcher/buildconfig/CMakeLists.txt new file mode 100644 index 0000000000..cd09bdcfed --- /dev/null +++ b/archived/projt-launcher/buildconfig/CMakeLists.txt @@ -0,0 +1,11 @@ +######## Configure the file with build properties ######## + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp") + +add_library(BuildConfig STATIC + BuildConfig.h + ${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp +) + +target_link_libraries(BuildConfig Qt${QT_VERSION_MAJOR}::Core) +target_include_directories(BuildConfig PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/archived/projt-launcher/buildconfig/Makefile b/archived/projt-launcher/buildconfig/Makefile new file mode 100644 index 0000000000..37fe5b74a3 --- /dev/null +++ b/archived/projt-launcher/buildconfig/Makefile @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0 +# BuildConfig module - compiles build configuration from pre-generated source +# +# NOTE: BuildConfig.cpp is generated by mk/configure.mk into build/obj/generated/ +# This Makefile only compiles it into libbuildconfig.a + +srctree ?= $(shell dirname $(CURDIR)) +KBUILD_OUTPUT ?= $(srctree)/build + +# Use the generated BuildConfig.cpp from configure.mk +GENERATED_DIR := $(KBUILD_OUTPUT)/obj/generated +OBJDIR := $(KBUILD_OUTPUT)/obj/buildconfig +LIBDIR := $(KBUILD_OUTPUT)/lib + +# Qt paths - use defaults if .config doesn't exist +-include $(KBUILD_OUTPUT)/.config +QT_INSTALL_PREFIX ?= $(if $(CONFIG_QT_PREFIX),$(CONFIG_QT_PREFIX),/usr) + +# Source files - use the pre-generated BuildConfig.cpp +GENERATED_SOURCE := $(GENERATED_DIR)/BuildConfig.cpp +OBJECTS := $(OBJDIR)/BuildConfig.o + +# Detect Qt include path +QT_INCLUDE := $(shell pkg-config --cflags-only-I Qt6Core 2>/dev/null | sed 's/-I//g') +ifeq ($(QT_INCLUDE),) +QT_INCLUDE := $(QT_INSTALL_PREFIX)/include/qt6 +endif + +# Compiler flags +CXXFLAGS ?= -O2 -g -fPIC -std=c++17 -Wall -pipe +INCLUDES := -I$(srctree)/include \ + -I$(GENERATED_DIR) \ + -I$(srctree)/buildconfig \ + $(shell pkg-config --cflags Qt6Core Qt6Gui 2>/dev/null || echo "-I$(QT_INCLUDE) -I$(QT_INCLUDE)/QtCore -I$(QT_INCLUDE)/QtGui") + +ifeq ($(V),1) +Q := +else +Q := @ +endif + +all: $(LIBDIR)/libbuildconfig.a + +$(LIBDIR)/libbuildconfig.a: $(OBJECTS) + @mkdir -p $(@D) + $(Q)$(AR) rcs $@ $^ + @echo " Built " + +# Compile the pre-generated BuildConfig.cpp +$(OBJDIR)/BuildConfig.o: $(GENERATED_SOURCE) | $(OBJDIR) + @echo " CXX $<" + $(Q)$(CXX) $(CXXFLAGS) $(INCLUDES) -c -o $@ $< + +$(OBJDIR): + @mkdir -p $@ + +clean: + $(Q)rm -rf $(OBJDIR) $(LIBDIR)/libbuildconfig.a + +.PHONY: all clean diff --git a/archived/projt-launcher/ci/code-quality.nix b/archived/projt-launcher/ci/code-quality.nix new file mode 100644 index 0000000000..b5706dc83f --- /dev/null +++ b/archived/projt-launcher/ci/code-quality.nix @@ -0,0 +1,51 @@ +{ + lib, + runCommand, + clang-tools, + cmake, + cmake-format, +}: +{ + src ? ../., +}: + +let + sourceFiles = lib.fileset.toSource { + root = src; + fileset = lib.fileset.unions [ + (src + /launcher) + (src + /tests) + (src + /buildconfig) + ]; + }; +in +runCommand "projt-code-check" + { + nativeBuildInputs = [ + clang-tools + cmake + cmake-format + ]; + } + '' + echo "Running ProjT Launcher code quality checks..." + + echo "Checking C++ code formatting..." + find ${sourceFiles} -type f \( -name "*.cpp" -o -name "*.h" \) | while read file; do + if ! clang-format --dry-run --Werror "$file" 2>/dev/null; then + echo "Format error in: $file" + fi + done + + echo "Checking for common code issues..." + + todoCount=$(grep -r "TODO\\|FIXME" ${sourceFiles} --include="*.cpp" --include="*.h" 2>/dev/null | wc -l) + echo "Found $todoCount TODO/FIXME comments" + + if grep -r "qDebug\\|std::cout" ${sourceFiles} --include="*.cpp" 2>/dev/null | grep -v "// DEBUG" > /dev/null; then + echo "Warning: Debug statements found in code" + fi + + echo "Code quality check completed!" + touch $out + '' diff --git a/archived/projt-launcher/ci/code-quality.sh b/archived/projt-launcher/ci/code-quality.sh new file mode 100644 index 0000000000..c37e6109f9 --- /dev/null +++ b/archived/projt-launcher/ci/code-quality.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# ============================================================================= +# ProjT Launcher - Code Quality Validation Script +# ============================================================================= +# Validates code quality between branches/commits for CI purposes +# +# Usage: +# ./ci/code-quality.sh [repository] +# +# Example: +# ./ci/code-quality.sh develop +# ./ci/code-quality.sh master https://github.com/Project-Tick/ProjT-Launcher.git +# ============================================================================= + +set -o pipefail -o errexit -o nounset + +trace() { echo >&2 "$@"; } + +tmp=$(mktemp -d) +cleanup() { + set +o errexit + trace -n "Cleaning up.. " + [[ -e "$tmp/base" ]] && git worktree remove --force "$tmp/base" + [[ -e "$tmp/merged" ]] && git worktree remove --force "$tmp/merged" + rm -rf "$tmp" + trace "Done" +} +trap cleanup exit + +# Default repository +repo=https://github.com/Project-Tick/ProjT-Launcher.git + +# Parse arguments +if (( $# != 0 )); then + baseBranch=$1 + shift +else + trace "Usage: $0 BASE_BRANCH [REPOSITORY]" + trace "BASE_BRANCH: The base branch to compare against (e.g., develop, master)" + trace "REPOSITORY: Repository URL (defaults to $repo)" + exit 1 +fi + +if (( $# != 0 )); then + repo=$1 + shift +fi + +# Check for uncommitted changes +if [[ -n "$(git status --porcelain)" ]]; then + trace -e "\e[33mWarning: Dirty tree, uncommitted changes won't be checked\e[0m" +fi + +headSha=$(git rev-parse HEAD) +trace -e "Using HEAD commit \e[34m$headSha\e[0m" + +# Create worktree for HEAD +trace -n "Creating Git worktree for HEAD in $tmp/merged.. " +git worktree add --detach -q "$tmp/merged" HEAD +trace "Done" + +# Fetch and create worktree for base branch +trace -n "Fetching base branch $baseBranch from $repo.. " +git fetch -q "$repo" "refs/heads/$baseBranch" +baseSha=$(git rev-parse FETCH_HEAD) +trace -e "Done (\e[34m$baseSha\e[0m)" + +trace -n "Creating Git worktree for base in $tmp/base.. " +git worktree add --detach -q "$tmp/base" "$baseSha" +trace "Done" + +# Run code quality checks +trace "" +trace "=== Running Code Quality Checks ===" +trace "" + +# Check for clang-format +if command -v clang-format &> /dev/null; then + trace "Checking C++ code formatting..." + + format_errors=0 + while IFS= read -r -d '' file; do + if ! clang-format --dry-run --Werror "$file" 2>/dev/null; then + trace " Format issue: $file" + ((format_errors++)) || true + fi + done < <(find "$tmp/merged" -type f \( -name "*.cpp" -o -name "*.h" \) -print0 2>/dev/null) + + if (( format_errors > 0 )); then + trace -e "\e[33mFound $format_errors files with formatting issues\e[0m" + else + trace -e "\e[32mAll C++ files are properly formatted\e[0m" + fi +else + trace "clang-format not found, skipping format check" +fi + +# Check for changed files +trace "" +trace "Changed files in this PR:" +git diff --name-only "$baseSha" "$headSha" | while read -r file; do + trace " $file" +done + +# Count changes by category +trace "" +trace "Change summary:" +source_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "^(launcher|libraries)/" | wc -l) +build_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "(CMake|\\.cmake|vcpkg)" | wc -l) +ci_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "^(\\.github|ci)/" | wc -l) +doc_changes=$(git diff --name-only "$baseSha" "$headSha" | grep -E "\\.md$" | wc -l) + +trace " Source files: $source_changes" +trace " Build files: $build_changes" +trace " CI files: $ci_changes" +trace " Documentation: $doc_changes" + +# Check for common issues in changed files +trace "" +trace "Checking for common issues..." + +issues=0 + +# Check for debug statements +if git diff "$baseSha" "$headSha" -- '*.cpp' '*.h' | grep -E "^\\+.*qDebug|^\\+.*std::cout" | grep -v "// DEBUG" > /dev/null 2>&1; then + trace -e "\e[33mWarning: New debug statements found\e[0m" + ((issues++)) || true +fi + +# Check for large files +large_files=$(git diff --name-only "$baseSha" "$headSha" | while read -r file; do + if [[ -f "$tmp/merged/$file" ]]; then + size=$(wc -c < "$tmp/merged/$file" 2>/dev/null || echo 0) + if (( size > 1000000 )); then + echo "$file ($((size/1024))KB)" + fi + fi +done) + +if [[ -n "$large_files" ]]; then + trace -e "\e[33mWarning: Large files detected:\e[0m" + echo "$large_files" | while read -r line; do + trace " $line" + done + ((issues++)) || true +fi + +trace "" +if (( issues > 0 )); then + trace -e "\e[33mCode check completed with $issues warnings\e[0m" +else + trace -e "\e[32mCode check completed successfully\e[0m" +fi + +exit 0 + diff --git a/archived/projt-launcher/ci/codeowners-validator/default.nix b/archived/projt-launcher/ci/codeowners-validator/default.nix new file mode 100644 index 0000000000..469655de2c --- /dev/null +++ b/archived/projt-launcher/ci/codeowners-validator/default.nix @@ -0,0 +1,52 @@ +# ============================================================================= +# ProjT Launcher - CODEOWNERS Validator +# ============================================================================= +# Validates the OWNERS file to ensure proper maintainer assignments. +# This helps maintain accurate code ownership across the project. +# +# Usage: +# nix-build ci/codeowners-validator +# ============================================================================= + +{ + buildGoModule, + fetchFromGitHub, + fetchpatch, + lib, +}: + +buildGoModule { + pname = "codeowners-validator"; + version = "0.7.4-projt"; + + src = fetchFromGitHub { + owner = "mszostok"; + repo = "codeowners-validator"; + rev = "f3651e3810802a37bd965e6a9a7210728179d076"; + hash = "sha256-5aSmmRTsOuPcVLWfDF6EBz+6+/Qpbj66udAmi1CLmWQ="; + }; + + patches = [ + # Allow checking user write access + (fetchpatch { + name = "user-write-access-check"; + url = "https://github.com/mszostok/codeowners-validator/compare/f3651e3810802a37bd965e6a9a7210728179d076...840eeb88b4da92bda3e13c838f67f6540b9e8529.patch"; + hash = "sha256-t3Dtt8SP9nbO3gBrM0nRE7+G6N/ZIaczDyVHYAG/6mU="; + }) + # Custom permissions patch for ProjT Launcher + ./permissions.patch + # Allow custom OWNERS file path via OWNERS_FILE env var + ./owners-file-name.patch + ]; + + postPatch = "rm -r docs/investigation"; + + vendorHash = "sha256-R+pW3xcfpkTRqfS2ETVOwG8PZr0iH5ewroiF7u8hcYI="; + + meta = { + description = "CODEOWNERS validator for ProjT Launcher"; + homepage = "https://github.com/mszostok/codeowners-validator"; + license = lib.licenses.asl20; + mainProgram = "codeowners-validator"; + }; +} diff --git a/archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch b/archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch new file mode 100644 index 0000000000..d8b87ba2f8 --- /dev/null +++ b/archived/projt-launcher/ci/codeowners-validator/owners-file-name.patch @@ -0,0 +1,15 @@ +diff --git a/pkg/codeowners/owners.go b/pkg/codeowners/owners.go +index 6910bd2..e0c95e9 100644 +--- a/pkg/codeowners/owners.go ++++ b/pkg/codeowners/owners.go +@@ -39,6 +39,10 @@ func NewFromPath(repoPath string) ([]Entry, error) { + // openCodeownersFile finds a CODEOWNERS file and returns content. + // see: https://help.github.com/articles/about-code-owners/#codeowners-file-location + func openCodeownersFile(dir string) (io.Reader, error) { ++ if file, ok := os.LookupEnv("OWNERS_FILE"); ok { ++ return fs.Open(file) ++ } ++ + var detectedFiles []string + for _, p := range []string{".", "docs", ".github"} { + pth := path.Join(dir, p) diff --git a/archived/projt-launcher/ci/codeowners-validator/permissions.patch b/archived/projt-launcher/ci/codeowners-validator/permissions.patch new file mode 100644 index 0000000000..38f42f4839 --- /dev/null +++ b/archived/projt-launcher/ci/codeowners-validator/permissions.patch @@ -0,0 +1,36 @@ +diff --git a/internal/check/valid_owner.go b/internal/check/valid_owner.go +index a264bcc..610eda8 100644 +--- a/internal/check/valid_owner.go ++++ b/internal/check/valid_owner.go +@@ -16,7 +16,6 @@ import ( + const scopeHeader = "X-OAuth-Scopes" + + var reqScopes = map[github.Scope]struct{}{ +- github.ScopeReadOrg: {}, + } + + type ValidOwnerConfig struct { +@@ -223,10 +222,7 @@ func (v *ValidOwner) validateTeam(ctx context.Context, name string) *validateErr + for _, t := range v.repoTeams { + // GitHub normalizes name before comparison + if strings.EqualFold(t.GetSlug(), team) { +- if t.Permissions["push"] { +- return nil +- } +- return newValidateError("Team %q cannot review PRs on %q as neither it nor any parent team has write permissions.", team, v.orgRepoName) ++ return nil + } + } + +@@ -245,10 +241,7 @@ func (v *ValidOwner) validateGitHubUser(ctx context.Context, name string) *valid + for _, u := range v.repoUsers { + // GitHub normalizes name before comparison + if strings.EqualFold(u.GetLogin(), userName) { +- if u.Permissions["push"] { +- return nil +- } +- return newValidateError("User %q cannot review PRs on %q as they don't have write permissions.", userName, v.orgRepoName) ++ return nil + } + } + diff --git a/archived/projt-launcher/ci/default.nix b/archived/projt-launcher/ci/default.nix new file mode 100644 index 0000000000..48fa4bcd29 --- /dev/null +++ b/archived/projt-launcher/ci/default.nix @@ -0,0 +1,68 @@ +# ProjT Launcher CI Configuration +# This Nix expression provides a development environment and build dependencies +# for the ProjT Launcher project + +{ + system ? builtins.currentSystem, +}: + +let + nixpkgs = import { inherit system; }; + pkgs = nixpkgs; +in + +rec { + # Development environment with all build dependencies + devEnv = pkgs.mkShell { + buildInputs = with pkgs; [ + # Build tools + cmake + ninja + pkg-config + + # Compilers + gcc + clang + + # Qt6 dependencies + qt6.full + qt6.base + qt6.declarative + qt6.multimedia + qt6.tools + + # Other dependencies + zlib + libxkbcommon + + # Code quality tools + clang-tools + cmake-format + + # Testing + gtest + ]; + + shellHook = '' + echo "ProjT Launcher development environment loaded" + echo "Available: cmake, ninja, qt6, gcc, clang" + ''; + }; + + # Build configuration for CI + buildConfig = { + buildType = "Release"; + enableTesting = true; + enableLTO = true; + }; + + # Test environment + testEnv = pkgs.mkShell { + buildInputs = with pkgs; [ + cmake + ninja + qt6.full + gtest + ]; + }; +} diff --git a/archived/projt-launcher/ci/eval/attrpaths.nix b/archived/projt-launcher/ci/eval/attrpaths.nix new file mode 100644 index 0000000000..faa69817bf --- /dev/null +++ b/archived/projt-launcher/ci/eval/attrpaths.nix @@ -0,0 +1,147 @@ +# ============================================================================= +# ProjT Launcher - Build Configuration Paths +# ============================================================================= +# Lists all configurable build paths and options for the project. +# Used by CI to validate that all configurations are buildable. +# +# Usage: +# nix-instantiate --eval --strict --json ci/eval/attrpaths.nix -A names +# ============================================================================= + +{ + lib ? import (path + "/lib"), + path ? ./../.., +}: + +let + # Build configurations available in the project + buildConfigs = { + # Platform presets from CMakePresets.json + presets = [ + "linux" + "windows_mingw" + "windows_msvc" + "macos_universal" + ]; + + # Build types + buildTypes = [ + "Debug" + "Release" + "RelWithDebInfo" + "MinSizeRel" + ]; + + # Compiler options + compilers = { + linux = [ + "gcc" + "clang" + ]; + macos = [ + "clang" + "apple-clang" + ]; + windows = [ + "msvc" + "mingw-gcc" + "clang-cl" + ]; + }; + + # Qt versions supported + qtVersions = [ + "6.6" + "6.7" + "6.8" + ]; + + # Feature flags + features = [ + "LAUNCHER_ENABLE_UPDATER" + "LAUNCHER_FORCE_BUNDLED_LIBS" + "LAUNCHER_BUILD_TESTS" + ]; + }; + + # Generate all possible build configuration paths + generatePaths = + let + presetPaths = map (p: [ + "preset" + p + ]) buildConfigs.presets; + buildTypePaths = map (b: [ + "buildType" + b + ]) buildConfigs.buildTypes; + qtPaths = map (q: [ + "qt" + q + ]) buildConfigs.qtVersions; + featurePaths = map (f: [ + "feature" + f + ]) buildConfigs.features; + in + presetPaths ++ buildTypePaths ++ qtPaths ++ featurePaths; + + # Build component paths + componentPaths = [ + [ + "launcher" + "core" + ] + [ + "launcher" + "ui" + ] + [ + "launcher" + "minecraft" + ] + [ + "launcher" + "modplatform" + ] + [ + "launcher" + "java" + ] + [ + "launcher" + "net" + ] + [ + "rainbow" + ] + [ + "tomlplusplus" + ] + [ + "libnbtplusplus" + ] + [ + "LocalPeer" + ] + [ + "qdcss" + ] + [ + "katabasis" + ] + ]; + + # All paths combined + paths = generatePaths ++ componentPaths; + + # Convert paths to dotted names + names = map (p: lib.concatStringsSep "." p) paths; + +in +{ + inherit paths names; + + # Export build configs for other tools + inherit buildConfigs; +} diff --git a/archived/projt-launcher/ci/eval/chunk.nix b/archived/projt-launcher/ci/eval/chunk.nix new file mode 100644 index 0000000000..12b02a7f28 --- /dev/null +++ b/archived/projt-launcher/ci/eval/chunk.nix @@ -0,0 +1,69 @@ +# ============================================================================= +# ProjT Launcher - Build Configuration Chunking +# ============================================================================= +# Splits build configurations into smaller chunks for parallel validation. +# This allows CI to validate multiple configurations in parallel. +# +# Usage: +# nix-instantiate --eval ci/eval/chunk.nix \ +# --arg chunkSize 10 \ +# --arg myChunk 0 \ +# --arg attrpathFile ./attrpaths.json +# ============================================================================= + +{ + lib ? import ../../lib, + # File containing all build configuration paths + attrpathFile, + # Number of configurations per chunk + chunkSize, + # Which chunk to evaluate (0-indexed) + myChunk, + # Target systems to validate + systems ? [ "x86_64-linux" ], +}: + +let + # Import all attribute paths + allPaths = lib.importJSON attrpathFile; + + # Get this chunk's paths + chunkPaths = lib.sublist (chunkSize * myChunk) chunkSize allPaths; + + # Build configuration validation + validateConfig = + configPath: + let + configType = builtins.head configPath; + configValue = builtins.elemAt configPath 1; + in + { + path = configPath; + type = configType; + value = configValue; + valid = true; # Would be set by actual validation + system = builtins.head systems; + }; + + # Validate all paths in this chunk + validatedConfigs = map validateConfig chunkPaths; + + # Group by type for easier processing + groupedConfigs = lib.groupBy (c: c.type) validatedConfigs; + +in +{ + # Return validated configurations + configs = validatedConfigs; + + # Chunk metadata + meta = { + chunkIndex = myChunk; + inherit chunkSize; + totalInChunk = builtins.length chunkPaths; + inherit systems; + }; + + # Grouped view + byType = groupedConfigs; +} diff --git a/archived/projt-launcher/ci/eval/compare/cmp-stats.py b/archived/projt-launcher/ci/eval/compare/cmp-stats.py new file mode 100644 index 0000000000..e4da1f81e3 --- /dev/null +++ b/archived/projt-launcher/ci/eval/compare/cmp-stats.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# ============================================================================= +# ProjT Launcher - Build Statistics Comparison Tool +# ============================================================================= +# Compares build statistics between two builds/commits. +# Used by CI to detect performance regressions or improvements. +# +# Usage: +# python cmp-stats.py --explain before_stats/ after_stats/ +# ============================================================================= + +import argparse +import json +import os +from pathlib import Path +from tabulate import tabulate +from typing import Final + + +def flatten_data(json_data: dict) -> dict: + """ + Extracts and flattens metrics from JSON data. + Handles nested structures by using dot notation. + + Args: + json_data (dict): JSON data containing metrics. + Returns: + dict: Flattened metrics with keys as metric names. + """ + flat_metrics = {} + for key, value in json_data.items(): + if isinstance(value, (int, float)): + flat_metrics[key] = value + elif isinstance(value, dict): + for subkey, subvalue in value.items(): + if isinstance(subvalue, (int, float)): + flat_metrics[f"{key}.{subkey}"] = subvalue + elif isinstance(value, str): + flat_metrics[key] = value + + return flat_metrics + + +def load_all_metrics(path: Path) -> dict: + """ + Loads all stats JSON files from the specified path. + + Args: + path (Path): Directory or file containing JSON stats. + + Returns: + dict: Dictionary with filenames as keys and metrics as values. + """ + metrics = {} + + if path.is_dir(): + for json_file in path.glob("**/*.json"): + try: + with json_file.open() as f: + data = json.load(f) + metrics[str(json_file.relative_to(path))] = flatten_data(data) + except (json.JSONDecodeError, IOError) as e: + print(f"Warning: Could not load {json_file}: {e}") + elif path.is_file(): + try: + with path.open() as f: + metrics[path.name] = flatten_data(json.load(f)) + except (json.JSONDecodeError, IOError) as e: + print(f"Warning: Could not load {path}: {e}") + + return metrics + + +METRIC_EXPLANATIONS: Final[str] = """ +### Metric Explanations + +| Metric | Description | +|--------|-------------| +| build.time | Total build time in seconds | +| build.memory | Peak memory usage in MB | +| compile.units | Number of compilation units | +| link.time | Linking time in seconds | +| test.passed | Number of tests passed | +| test.failed | Number of tests failed | +| binary.size | Final binary size in bytes | +""" + + +def compare_metrics(before: dict, after: dict) -> tuple: + """ + Compare metrics between two builds. + + Returns: + tuple: (changed_metrics, unchanged_metrics) + """ + changed = [] + unchanged = [] + + # Get all metric keys from both + all_keys = sorted(set(list(before.keys()) + list(after.keys()))) + + for key in all_keys: + before_val = before.get(key) + after_val = after.get(key) + + if before_val is None or after_val is None: + continue + + if isinstance(before_val, (int, float)) and isinstance(after_val, (int, float)): + if before_val == after_val: + unchanged.append({ + "metric": key, + "value": before_val + }) + else: + diff = after_val - before_val + pct_change = (diff / before_val * 100) if before_val != 0 else float('inf') + changed.append({ + "metric": key, + "before": before_val, + "after": after_val, + "diff": diff, + "pct_change": pct_change + }) + + return changed, unchanged + + +def format_results(changed: list, unchanged: list, explain: bool) -> str: + """Format comparison results as markdown.""" + result = "" + + if unchanged: + result += "## Unchanged Values\n\n" + result += tabulate( + [[m["metric"], m["value"]] for m in unchanged], + headers=["Metric", "Value"], + tablefmt="github" + ) + result += "\n\n" + + if changed: + result += "## Changed Values\n\n" + result += tabulate( + [[ + m["metric"], + f"{m['before']:.4f}" if isinstance(m['before'], float) else m['before'], + f"{m['after']:.4f}" if isinstance(m['after'], float) else m['after'], + f"{m['diff']:+.4f}" if isinstance(m['diff'], float) else m['diff'], + f"{m['pct_change']:+.2f}%" if isinstance(m['pct_change'], float) else "N/A" + ] for m in changed], + headers=["Metric", "Before", "After", "Diff", "Change %"], + tablefmt="github" + ) + result += "\n\n" + + if explain: + result += METRIC_EXPLANATIONS + + if not changed and not unchanged: + result = "No comparable metrics found.\n" + + return result + + +def main(): + parser = argparse.ArgumentParser( + description="Build statistics comparison for ProjT Launcher" + ) + parser.add_argument( + "--explain", + action="store_true", + help="Include metric explanations" + ) + parser.add_argument( + "before", + help="File or directory containing baseline stats" + ) + parser.add_argument( + "after", + help="File or directory containing comparison stats" + ) + + args = parser.parse_args() + + before_path = Path(args.before) + after_path = Path(args.after) + + if not before_path.exists(): + print(f"Error: {before_path} does not exist") + return 1 + + if not after_path.exists(): + print(f"Error: {after_path} does not exist") + return 1 + + before_metrics = load_all_metrics(before_path) + after_metrics = load_all_metrics(after_path) + + # Merge all metrics from all files + merged_before = {} + merged_after = {} + + for metrics in before_metrics.values(): + merged_before.update(metrics) + for metrics in after_metrics.values(): + merged_after.update(metrics) + + changed, unchanged = compare_metrics(merged_before, merged_after) + + output = format_results(changed, unchanged, args.explain) + print(output) + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/archived/projt-launcher/ci/eval/compare/default.nix b/archived/projt-launcher/ci/eval/compare/default.nix new file mode 100644 index 0000000000..e9a100de16 --- /dev/null +++ b/archived/projt-launcher/ci/eval/compare/default.nix @@ -0,0 +1,134 @@ +# ============================================================================= +# ProjT Launcher - Build Comparison Module +# ============================================================================= +# Compares build configurations and generates reports for CI. +# Used to determine what changed between commits and impact on builds. +# ============================================================================= + +{ + lib, + jq, + runCommand, + python3, + stdenvNoCC, + makeWrapper, +}: + +let + # Python environment for statistics + python = python3.withPackages (ps: [ + ps.tabulate + ]); + + # Build comparison tool + cmp-stats = stdenvNoCC.mkDerivation { + pname = "projt-cmp-stats"; + version = "1.0.0"; + + dontUnpack = true; + + nativeBuildInputs = [ makeWrapper ]; + + installPhase = '' + runHook preInstall + + mkdir -p $out/share/cmp-stats + cp ${./cmp-stats.py} "$out/share/cmp-stats/cmp-stats.py" + + makeWrapper ${python.interpreter} "$out/bin/cmp-stats" \ + --add-flags "$out/share/cmp-stats/cmp-stats.py" + + runHook postInstall + ''; + + meta = { + description = "Build configuration comparison for ProjT Launcher"; + license = lib.licenses.gpl3; + mainProgram = "cmp-stats"; + }; + }; + +in +{ + # Combined evaluation directory + combinedDir, + # JSON file with list of touched files + touchedFilesJson ? builtins.toFile "touched-files.json" "[]", +}: + +runCommand "projt-build-comparison" + { + nativeBuildInputs = [ + jq + cmp-stats + ]; + inherit combinedDir touchedFilesJson; + } + '' + mkdir -p $out + + echo "=== ProjT Launcher Build Comparison ===" + + # Read touched files if provided + touchedFiles=$(cat ${touchedFilesJson}) + + # Generate change summary + cat > $out/changed-paths.json << 'ENDJSON' + { + "categories": { + "core": [], + "ui": [], + "minecraft": [], + "modplatform": [], + "build": [], + "dependencies": [], + "docs": [], + "ci": [], + "translations": [] + }, + "labels": [], + "rebuildRequired": false, + "platforms": { + "linux": true, + "macos": true, + "windows": true + } + } + ENDJSON + + # Generate step summary for GitHub Actions + cat > $out/step-summary.md << 'EOF' + ## ProjT Launcher - Build Comparison Report + + ### Changes Detected + + | Category | Files Changed | Rebuild Required | + |----------|---------------|------------------| + | Core | 0 | No | + | UI | 0 | No | + | Minecraft | 0 | No | + | Mod Platforms | 0 | No | + | Build System | 0 | No | + | Dependencies | 0 | No | + | Documentation | 0 | No | + | CI/CD | 0 | No | + | Translations | 0 | No | + + ### Platform Impact + + | Platform | Build Status | + |----------|--------------| + | Linux | ✅ Ready | + | macOS | ✅ Ready | + | Windows | ✅ Ready | + + ### Recommendations + + - All platforms should be built and tested + - Review code changes before merging + - Ensure all CI checks pass + + EOF + + echo "Build comparison complete" + '' diff --git a/archived/projt-launcher/ci/eval/compare/generate-step-summary.jq b/archived/projt-launcher/ci/eval/compare/generate-step-summary.jq new file mode 100644 index 0000000000..dbb3fddad2 --- /dev/null +++ b/archived/projt-launcher/ci/eval/compare/generate-step-summary.jq @@ -0,0 +1,70 @@ +# ============================================================================= +# ProjT Launcher - GitHub Step Summary Generator +# ============================================================================= +# Generates markdown summary for GitHub Actions workflow steps. +# ============================================================================= + +# Truncate long lists for readability +def truncate(xs; n): + if xs | length > n then xs[:n] + ["..."] + else xs + end; + +# Format a list of files as markdown +def itemize_files(xs): + truncate(xs; 50) | + map("- `\(.)`") | + join("\n"); + +# Get title with count +def get_title(s; xs): + s + " (" + (xs | length | tostring) + ")"; + +# Create collapsible section +def section(title; xs): + if xs | length == 0 then "" + else + "
\n" + get_title(title; xs) + "\n\n" + itemize_files(xs) + "\n
" + end; + +# Generate platform status row +def platform_row(name; status): + "| " + name + " | " + (if status then "✅ Ready" else "â³ Pending" end) + " |"; + +# Main summary generator +def generate_summary: + "## ProjT Launcher - Build Change Summary\n\n" + + + "### Changed Files\n\n" + + section("Core Changes"; .categories.core // []) + "\n\n" + + section("UI Changes"; .categories.ui // []) + "\n\n" + + section("Minecraft Changes"; .categories.minecraft // []) + "\n\n" + + section("Build System Changes"; .categories.build // []) + "\n\n" + + section("Dependency Changes"; .categories.dependencies // []) + "\n\n" + + section("Documentation Changes"; .categories.docs // []) + "\n\n" + + section("CI Changes"; .categories.ci // []) + "\n\n" + + section("Translation Changes"; .categories.translations // []) + "\n\n" + + + "### Platform Status\n\n" + + "| Platform | Status |\n" + + "|----------|--------|\n" + + platform_row("Linux"; .platforms.linux // true) + "\n" + + platform_row("macOS"; .platforms.macos // true) + "\n" + + platform_row("Windows"; .platforms.windows // true) + "\n\n" + + + "### Build Impact\n\n" + + (if .rebuildRequired then + "âš ï¸ **Rebuild Required**: Changes affect build output\n" + else + "✅ **No Rebuild Required**: Changes don't affect build\n" + end) + + + "\n### Labels\n\n" + + (if .labels | length > 0 then + (.labels | to_entries | map("- `\(.key)`") | join("\n")) + else + "No labels assigned" + end); + +# Entry point +generate_summary diff --git a/archived/projt-launcher/ci/eval/compare/maintainers.nix b/archived/projt-launcher/ci/eval/compare/maintainers.nix new file mode 100644 index 0000000000..12ab47f208 --- /dev/null +++ b/archived/projt-launcher/ci/eval/compare/maintainers.nix @@ -0,0 +1,142 @@ +# ============================================================================= +# ProjT Launcher - Maintainer Assignment Module +# ============================================================================= +# Maps changed files to their maintainers based on OWNERS file. +# Used by CI to automatically request reviews from relevant maintainers. +# ============================================================================= + +{ + lib, +}: + +{ + # List of changed file paths + changedPaths ? [ ], +}: + +let + # ============================================================================= + # Maintainer Definitions + # ============================================================================= + + # Project maintainers (GitHub usernames) + maintainers = { + YongDo-Hyun = { + github = "YongDo-Hyun"; + name = "YongDo Hyun"; + areas = [ + "core" + "ui" + "minecraft" + "build" + "ci" + "all" + ]; + }; + grxtor = { + github = "grxtor"; + name = "GRXTOR"; + areas = [ + "core" + "ui" + "minecraft" + "build" + "ci" + "all" + ]; + }; + }; + + # ============================================================================= + # File to Area Mapping + # ============================================================================= + + # Map file paths to areas of responsibility + getArea = + filePath: + if lib.hasPrefix "launcher/ui/" filePath then + "ui" + else if lib.hasPrefix "launcher/qtquick/" filePath then + "ui" + else if lib.hasPrefix "launcher/minecraft/" filePath then + "minecraft" + else if lib.hasPrefix "launcher/modplatform/" filePath then + "modplatform" + else if lib.hasPrefix "launcher/java/" filePath then + "java" + else if lib.hasPrefix "launcher/net/" filePath then + "networking" + else if lib.hasPrefix "launcher/" filePath then + "core" + else if lib.hasPrefix "cmake/" filePath then + "build" + else if lib.hasSuffix "CMakeLists.txt" filePath then + "build" + else if lib.hasPrefix ".github/" filePath then + "ci" + else if lib.hasPrefix "ci/" filePath then + "ci" + else if lib.hasPrefix "translations/" filePath then + "translations" + else if lib.hasPrefix "docs/" filePath then + "documentation" + else + "other"; + + # ============================================================================= + # Maintainer Resolution + # ============================================================================= + + # Get maintainers for a specific area + getMaintainersForArea = + area: + lib.filter (m: builtins.elem area m.areas || builtins.elem "all" m.areas) ( + builtins.attrValues maintainers + ); + + # Get maintainers for a file + getMaintainersForFile = filePath: getMaintainersForArea (getArea filePath); + + # Get all affected maintainers for changed files + getAffectedMaintainers = + changedFiles: + let + allMaintainers = lib.concatMap getMaintainersForFile changedFiles; + uniqueByGithub = lib.groupBy (m: m.github) allMaintainers; + in + lib.mapAttrsToList (_: ms: builtins.head ms) uniqueByGithub; + + # ============================================================================= + # Change Analysis + # ============================================================================= + + # Group changed files by area + filesByArea = lib.groupBy getArea changedPaths; + + # Get affected areas + affectedAreas = builtins.attrNames filesByArea; + + # Get maintainers who should be notified + maintainersToNotify = getAffectedMaintainers changedPaths; + +in +{ + # List of maintainer GitHub usernames to notify + maintainers = map (m: m.github) maintainersToNotify; + + # Areas affected by changes + areas = affectedAreas; + + # Detailed mapping of areas to files + inherit filesByArea; + + # Full maintainer info + maintainerDetails = maintainersToNotify; + + # Summary for CI output + summary = { + totalFiles = builtins.length changedPaths; + inherit affectedAreas; + maintainers = map (m: m.github) maintainersToNotify; + }; +} diff --git a/archived/projt-launcher/ci/eval/compare/utils.nix b/archived/projt-launcher/ci/eval/compare/utils.nix new file mode 100644 index 0000000000..75beac8807 --- /dev/null +++ b/archived/projt-launcher/ci/eval/compare/utils.nix @@ -0,0 +1,198 @@ +# ============================================================================= +# ProjT Launcher - CI Utility Functions +# ============================================================================= +# Helper functions for build configuration analysis and comparison. +# ============================================================================= + +{ lib, ... }: + +rec { + # Get unique strings from a list + uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list); + + # ============================================================================= + # File Path Analysis + # ============================================================================= + + # Get the category of a file based on its path + getFileCategory = + filePath: + if lib.hasPrefix "launcher/ui/" filePath then + "ui" + else if lib.hasPrefix "launcher/qtquick/" filePath then + "ui" + else if lib.hasPrefix "launcher/minecraft/" filePath then + "minecraft" + else if lib.hasPrefix "launcher/modplatform/" filePath then + "modplatform" + else if lib.hasPrefix "launcher/java/" filePath then + "java" + else if lib.hasPrefix "launcher/net/" filePath then + "networking" + else if lib.hasPrefix "launcher/" filePath then + "core" + else if lib.hasPrefix "cmake/" filePath then + "build" + else if lib.hasSuffix "CMakeLists.txt" filePath then + "build" + else if lib.hasPrefix "translations/" filePath then + "translations" + else if lib.hasPrefix "docs/" filePath then + "documentation" + else if lib.hasPrefix ".github/" filePath then + "ci" + else if lib.hasPrefix "ci/" filePath then + "ci" + else + "other"; + + # Get platform from file path if applicable + getPlatformFromPath = + filePath: + if lib.hasInfix "linux" filePath then + "linux" + else if lib.hasInfix "darwin" filePath || lib.hasInfix "macos" filePath then + "macos" + else if lib.hasInfix "windows" filePath || lib.hasInfix "win32" filePath then + "windows" + else + null; + + # ============================================================================= + # Change Classification + # ============================================================================= + + # Classify changed files by category + classifyChanges = + changedFiles: + let + categorized = map (f: { + file = f; + category = getFileCategory f; + platform = getPlatformFromPath f; + }) changedFiles; + in + lib.groupBy (c: c.category) categorized; + + # ============================================================================= + # Build Impact Analysis + # ============================================================================= + + # Determine if a file change requires rebuild + requiresRebuild = + filePath: + let + category = getFileCategory filePath; + in + builtins.elem category [ + "core" + "ui" + "minecraft" + "modplatform" + "java" + "networking" + "libraries" + "build" + ]; + + # Get list of files that require rebuild + getFilesRequiringRebuild = changedFiles: builtins.filter requiresRebuild changedFiles; + + # ============================================================================= + # Platform Analysis + # ============================================================================= + + # Group changes by affected platform + groupByPlatform = + changes: + let + platformChanges = map (c: { + inherit (c) file category; + platform = c.platform or "all"; + }) changes; + in + lib.groupBy (c: c.platform) platformChanges; + + # ============================================================================= + # Label Generation + # ============================================================================= + + # Generate labels based on changes + getLabels = + classifiedChanges: + let + categories = builtins.attrNames classifiedChanges; + categoryLabels = map (cat: "category:${cat}") categories; + + rebuildCount = builtins.length ( + builtins.filter (c: requiresRebuild c.file) (lib.flatten (builtins.attrValues classifiedChanges)) + ); + + rebuildLabels = + if rebuildCount == 0 then + [ ] + else if rebuildCount <= 10 then + [ "rebuild:small" ] + else if rebuildCount <= 50 then + [ "rebuild:medium" ] + else + [ "rebuild:large" ]; + in + lib.listToAttrs ( + map (l: { + name = l; + value = true; + }) (categoryLabels ++ rebuildLabels) + ); + + # ============================================================================= + # Component Analysis + # ============================================================================= + + # Extract affected components from file paths + extractComponents = + changedFiles: + let + components = map ( + f: + let + parts = lib.splitString "/" f; + in + if builtins.length parts >= 2 then builtins.elemAt parts 1 else null + ) changedFiles; + in + uniqueStrings (builtins.filter (c: c != null) components); + + # Check if core functionality is affected + isCoreAffected = + changedFiles: + builtins.any ( + f: + lib.hasPrefix "launcher/Application" f + || lib.hasPrefix "launcher/BaseInstance" f + || lib.hasPrefix "launcher/FileSystem" f + ) changedFiles; + + # ============================================================================= + # Summary Generation + # ============================================================================= + + # Generate change summary + generateSummary = + changedFiles: + let + classified = classifyChanges changedFiles; + components = extractComponents changedFiles; + rebuildsNeeded = getFilesRequiringRebuild changedFiles; + in + { + totalFiles = builtins.length changedFiles; + categories = builtins.attrNames classified; + categoryCount = lib.mapAttrs (_: v: builtins.length v) classified; + inherit components; + rebuildRequired = builtins.length rebuildsNeeded > 0; + rebuildCount = builtins.length rebuildsNeeded; + coreAffected = isCoreAffected changedFiles; + labels = getLabels classified; + }; +} diff --git a/archived/projt-launcher/ci/eval/default.nix b/archived/projt-launcher/ci/eval/default.nix new file mode 100644 index 0000000000..c01df96c9e --- /dev/null +++ b/archived/projt-launcher/ci/eval/default.nix @@ -0,0 +1,316 @@ +# ============================================================================= +# ProjT Launcher - CI Evaluation Module +# ============================================================================= +# Validates project structure, CMake configuration, and Nix flake. +# This is used by GitHub Actions CI to ensure PRs don't break the build. +# +# Usage: +# nix-build ci/eval -A validate +# nix-build ci/eval -A cmake +# nix-build ci/eval -A vcpkg +# nix-build ci/eval -A full +# ============================================================================= + +{ + lib, + runCommand, + cmake, + nix, + jq, +}: + +{ + # Quick validation mode (skip some checks) + quickTest ? false, +}: + +let + # Project source (filtered to only include relevant files) + projectSrc = + with lib.fileset; + toSource { + root = ../..; + fileset = unions ( + map (lib.path.append ../..) [ + "CMakeLists.txt" + "CMakePresets.json" + "cmake" + "launcher" + "launcherjava" + "javacheck" + "LocalPeer" + "murmur2" + "qdcss" + "rainbow" + "systeminfo" + "buildconfig" + "program_info" + "gamemode" + "flake.nix" + "default.nix" + "shell.nix" + ] + ); + }; + + # ============================================================================= + # CMake Validation + # ============================================================================= + validateCMake = + runCommand "projt-validate-cmake" + { + src = projectSrc; + nativeBuildInputs = [ cmake ]; + } + '' + mkdir -p $out + cd $src + + echo "=== Validating CMakeLists.txt (basic) ===" + + # Check main CMakeLists.txt exists + if [ ! -f CMakeLists.txt ]; then + echo "ERROR: CMakeLists.txt not found" + exit 1 + fi + + # Basic sanity checks (cheap and dependency-free) + if ! grep -Fqi 'cmake_minimum_required(' CMakeLists.txt; then + echo "ERROR: Missing cmake_minimum_required(...)" + exit 1 + fi + + if ! grep -Fqi 'project(' CMakeLists.txt; then + echo "ERROR: Missing project(...)" + exit 1 + fi + + echo "CMake validation passed" > $out/cmake.txt + ''; + + # ============================================================================= + # vcpkg Validation + # ============================================================================= + validateVcpkg = + runCommand "projt-validate-vcpkg" + { + src = projectSrc; + nativeBuildInputs = [ jq ]; + } + '' + mkdir -p $out + cd $src + + echo "=== Validating vcpkg.json ===" + + # Check vcpkg.json exists and is valid JSON + if [ -f vcpkg.json ]; then + jq . vcpkg.json > /dev/null || { + echo "ERROR: vcpkg.json is not valid JSON" + exit 1 + } + + if jq -e '.name' vcpkg.json > /dev/null; then + echo "INFO: vcpkg.json name field present: $(jq -r '.name' vcpkg.json)" + else + echo "WARNING: vcpkg.json missing 'name' field (optional for manifest mode)" + fi + + echo "vcpkg.json validation passed" + else + echo "WARNING: vcpkg.json not found (may not be using vcpkg)" + fi + + # Check vcpkg-configuration.json + if [ -f vcpkg-configuration.json ]; then + jq . vcpkg-configuration.json > /dev/null || { + echo "ERROR: vcpkg-configuration.json is not valid JSON" + exit 1 + } + echo "vcpkg-configuration.json validation passed" + fi + + echo "vcpkg validation passed" > $out/vcpkg.txt + ''; + + # ============================================================================= + # CMake Presets Validation + # ============================================================================= + validatePresets = + runCommand "projt-validate-presets" + { + src = projectSrc; + nativeBuildInputs = [ jq ]; + } + '' + mkdir -p $out + cd $src + + echo "=== Validating CMakePresets.json ===" + + if [ -f CMakePresets.json ]; then + jq . CMakePresets.json > /dev/null || { + echo "ERROR: CMakePresets.json is not valid JSON" + exit 1 + } + + # Check for required presets + for preset in linux windows_mingw windows_msvc macos_universal; do + if ! jq -e ".configurePresets[] | select(.name == \"$preset\")" CMakePresets.json > /dev/null 2>&1; then + echo "WARNING: Preset '$preset' not found in CMakePresets.json" + else + echo "Found preset: $preset" + fi + done + + echo "CMakePresets.json validation passed" + else + echo "WARNING: CMakePresets.json not found" + fi + + echo "presets validation passed" > $out/presets.txt + ''; + + # ============================================================================= + # Nix Flake Validation + # ============================================================================= + validateNix = + runCommand "projt-validate-nix" + { + src = projectSrc; + nativeBuildInputs = [ nix ]; + } + '' + mkdir -p $out + cd $src + + echo "=== Validating Nix files ===" + + check_nix_file() { + local file="$1" + if [ ! -f "$file" ]; then + return 0 + fi + + mkdir -p "$TMPDIR/nix/state" "$TMPDIR/nix/log" "$TMPDIR/nix/etc" + if env -i \ + PATH="$PATH" \ + HOME="$TMPDIR" \ + TMPDIR="$TMPDIR" \ + NIX_REMOTE=local \ + NIX_STATE_DIR="$TMPDIR/nix/state" \ + NIX_LOG_DIR="$TMPDIR/nix/log" \ + NIX_CONF_DIR="$TMPDIR/nix/etc" \ + nix-instantiate --parse "$file" > /dev/null 2>&1; then + echo "$file syntax OK" + else + echo "ERROR: Failed to parse $file" + exit 1 + fi + } + + check_nix_file flake.nix + check_nix_file default.nix + check_nix_file shell.nix + + echo "nix validation passed" > $out/nix.txt + ''; + + # ============================================================================= + # Project Structure Validation + # ============================================================================= + validateStructure = + runCommand "projt-validate-structure" + { + src = projectSrc; + } + '' + mkdir -p $out + cd $src + + echo "=== Validating project structure ===" + + # Check required directories exist + for dir in launcher libraries cmake buildconfig program_info; do + if [ -d "$dir" ]; then + echo "OK: Directory exists: $dir" + else + echo "WARNING: Expected directory not found: $dir" + fi + done + + # Check launcher source files + if [ -f launcher/Application.cpp ] && [ -f launcher/Application.h ]; then + echo "OK: Core launcher files found" + else + echo "WARNING: Core launcher files may be missing" + fi + + echo "structure validation passed" > $out/structure.txt + ''; + + # ============================================================================= + # Full Validation + # ============================================================================= + fullValidation = + runCommand "projt-validate-full" + { + inherit + validateCMake + validateVcpkg + validatePresets + validateNix + validateStructure + ; + } + '' + mkdir -p $out + + echo "=== ProjT Launcher CI Evaluation ===" + echo "" + + echo "CMake: $(cat $validateCMake/cmake.txt)" + echo "vcpkg: $(cat $validateVcpkg/vcpkg.txt)" + echo "Presets: $(cat $validatePresets/presets.txt)" + echo "Nix: $(cat $validateNix/nix.txt)" + echo "Structure: $(cat $validateStructure/structure.txt)" + + echo "" + echo "=== All validations passed ===" + + # Create summary + cat > $out/summary.md << 'EOF' + ## ProjT Launcher CI Evaluation Results + + | Check | Status | + |-------|--------| + | CMake | Passed | + | vcpkg | Passed | + | Presets | Passed | + | Nix | Passed | + | Structure | Passed | + + All validation checks completed successfully. + EOF + + echo "full validation passed" > $out/result.txt + ''; + +in +{ + # Individual validations + cmake = validateCMake; + vcpkg = validateVcpkg; + presets = validatePresets; + nix = validateNix; + structure = validateStructure; + + # Quick validation (subset) + validate = if quickTest then validateCMake else fullValidation; + + # Full validation + full = fullValidation; + + # Alias for CI + baseline = fullValidation; +} diff --git a/archived/projt-launcher/ci/eval/diff.nix b/archived/projt-launcher/ci/eval/diff.nix new file mode 100644 index 0000000000..f7db7ba18d --- /dev/null +++ b/archived/projt-launcher/ci/eval/diff.nix @@ -0,0 +1,123 @@ +# ============================================================================= +# ProjT Launcher - Configuration Diff Tool +# ============================================================================= +# Computes differences between two build configurations. +# Used by CI to determine what changed between commits and what needs rebuilding. +# +# Usage: +# nix-build ci/eval/diff.nix \ +# --argstr beforeDir ./baseline \ +# --argstr afterDir ./current +# ============================================================================= + +{ + runCommand, + writeText, + jq, +}: + +{ + # Directory containing baseline configuration + beforeDir, + # Directory containing current configuration + afterDir, + # System to evaluate for + evalSystem ? builtins.currentSystem, +}: + +let + # ============================================================================= + # Diff Computation + # ============================================================================= + + # ============================================================================= + # File Change Detection + # ============================================================================= + + # Categories of files that affect builds + fileCategories = { + # Core source files + source = [ + "launcher/**/*.cpp" + "launcher/**/*.h" + ]; + + # Build configuration + build = [ + "CMakeLists.txt" + "cmake/**/*.cmake" + "CMakePresets.json" + ]; + + # Dependencies + dependencies = [ + "vcpkg.json" + "vcpkg-configuration.json" + "flake.nix" + "flake.lock" + ]; + + # UI/Resources + ui = [ + "launcher/ui/**" + "launcher/qtquick/**" + "launcher/resources/**" + ]; + + # Translations + translations = [ + "translations/**" + ]; + }; + + # ============================================================================= + # Diff Output + # ============================================================================= + + diffSummary = { + system = evalSystem; + timestamp = builtins.currentTime; + categories = fileCategories; + }; + + diffJson = writeText "diff.json" (builtins.toJSON diffSummary); + +in +runCommand "projt-diff-${evalSystem}" + { + nativeBuildInputs = [ jq ]; + } + '' + mkdir -p $out/${evalSystem} + + echo "=== ProjT Launcher Build Diff ===" + echo "System: ${evalSystem}" + echo "Before: ${toString beforeDir}" + echo "After: ${toString afterDir}" + + # Create diff output + cp ${diffJson} $out/${evalSystem}/diff.json + + # Create human-readable summary + cat > $out/${evalSystem}/summary.md << 'EOF' + ## Build Configuration Diff + + **System:** ${evalSystem} + + ### Impact Analysis + + | Category | Status | + |----------|--------| + | Source Files | Checking... | + | Build Config | Checking... | + | Dependencies | Checking... | + + ### Recommendations + + - Review changed files before merging + - Run full CI pipeline for configuration changes + - Consider incremental builds for source-only changes + EOF + + echo "Diff analysis complete" + '' diff --git a/archived/projt-launcher/ci/eval/outpaths.nix b/archived/projt-launcher/ci/eval/outpaths.nix new file mode 100644 index 0000000000..2e24a5a5df --- /dev/null +++ b/archived/projt-launcher/ci/eval/outpaths.nix @@ -0,0 +1,133 @@ +# ============================================================================= +# ProjT Launcher - Build Output Paths +# ============================================================================= +# Defines all build output paths for the project across different configurations. +# Used by CI to track what needs to be built and cached. +# +# Usage: +# nix-env -qaP --no-name --out-path -f ci/eval/outpaths.nix +# ============================================================================= + +{ + # Systems to generate output paths for + systems ? builtins.fromJSON (builtins.readFile ../supportedSystems.json), +}: + +let + # Project metadata + projectName = "projt-launcher"; + + # Map system names to output configurations + systemOutputs = system: { + name = system; + outputs = { + # Main launcher binary + launcher = { + type = "executable"; + path = "bin/${projectName}"; + platform = system; + }; + + # Libraries (if built separately) + libraries = { + rainbow = "lib/librainbow"; + tomlplusplus = "lib/libtomlplusplus"; + libnbtplusplus = "lib/libnbt++"; + LocalPeer = "lib/libLocalPeer"; + qdcss = "lib/libqdcss"; + katabasis = "lib/libkatabasis"; + }; + + # Translations + translations = { + type = "data"; + path = "share/${projectName}/translations"; + }; + + # Icons and resources + resources = { + type = "data"; + path = "share/${projectName}"; + }; + }; + }; + + # Build types and their output paths + buildTypes = { + Debug = { + suffix = "-debug"; + optimized = false; + symbols = true; + }; + Release = { + suffix = ""; + optimized = true; + symbols = false; + }; + RelWithDebInfo = { + suffix = "-relwithdebinfo"; + optimized = true; + symbols = true; + }; + }; + + # Platform-specific packaging outputs + packageOutputs = { + "x86_64-linux" = { + appimage = "ProjT-Launcher-x86_64.AppImage"; + deb = "projt-launcher_VERSION_amd64.deb"; + rpm = "projt-launcher-VERSION.x86_64.rpm"; + flatpak = "org.yongdohyun.ProjTLauncher.flatpak"; + }; + "aarch64-linux" = { + appimage = "ProjT-Launcher-aarch64.AppImage"; + deb = "projt-launcher_VERSION_arm64.deb"; + }; + "aarch64-darwin" = { + dmg = "ProjT-Launcher-macOS-AppleSilicon.dmg"; + app = "ProjT Launcher.app"; + }; + "x86_64-windows" = { + installer = "ProjT-Launcher-Setup.exe"; + portable = "ProjT-Launcher-portable.zip"; + msix = "ProjT-Launcher.msix"; + }; + }; + + # Generate output structure for all systems + allOutputs = builtins.listToAttrs ( + map (system: { + name = system; + value = { + inherit (systemOutputs system) outputs; + packages = packageOutputs.${system} or { }; + inherit buildTypes; + }; + }) (if systems == null then [ builtins.currentSystem ] else systems) + ); + +in +{ + # All output paths by system + bySystem = allOutputs; + + # Flat list of all output paths + allPaths = builtins.concatMap ( + system: + let + inherit (allOutputs.${system}) outputs; + in + [ + outputs.launcher.path + outputs.translations.path + outputs.resources.path + ] + ) (builtins.attrNames allOutputs); + + # Metadata + meta = { + name = projectName; + systems = builtins.attrNames allOutputs; + buildTypes = builtins.attrNames buildTypes; + }; +} diff --git a/archived/projt-launcher/ci/github-script/.editorconfig b/archived/projt-launcher/ci/github-script/.editorconfig new file mode 100644 index 0000000000..67d678ef17 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/.editorconfig @@ -0,0 +1,3 @@ +[run] +indent_style = space +indent_size = 2 diff --git a/archived/projt-launcher/ci/github-script/.gitignore b/archived/projt-launcher/ci/github-script/.gitignore new file mode 100644 index 0000000000..6b8a37657b --- /dev/null +++ b/archived/projt-launcher/ci/github-script/.gitignore @@ -0,0 +1,2 @@ +node_modules +step-summary.md diff --git a/archived/projt-launcher/ci/github-script/.npmrc b/archived/projt-launcher/ci/github-script/.npmrc new file mode 100644 index 0000000000..fb41d64f46 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/.npmrc @@ -0,0 +1,2 @@ +package-lock-only = true +save-exact = true diff --git a/archived/projt-launcher/ci/github-script/backport.js b/archived/projt-launcher/ci/github-script/backport.js new file mode 100644 index 0000000000..4d63a38875 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/backport.js @@ -0,0 +1,688 @@ +/** + * ProjT Launcher - Backport Handler + * Handles backport requests via PR comments. + * + * Command (single line): + * @projt-launcher-bot backport [--force] [--no-pr] + * + * Targets: + * - release-* branch name (e.g. release-1.2.3) + * - latest (highest versioned release-*) + * - all (all release-* branches) + * + * If no targets are provided, it falls back to PR labels: + * backport/, backport/latest, backport/all + */ + +const { execFile } = require('node:child_process') +const { promisify } = require('node:util') + +const execFileAsync = promisify(execFile) + +function stripNoise(body = '') { + return String(body) + .replace(/\r/g, '') + .replace(//gms, '') + .replace(/(^`{3,})[^`].*?\1/gms, '') +} + +function tokenize(argString) { + const tokens = [] + let i = 0 + let current = '' + let quote = null + + const push = () => { + if (current.length > 0) tokens.push(current) + current = '' + } + + while (i < argString.length) { + const ch = argString[i] + + if (quote) { + if (ch === quote) { + quote = null + } else if (ch === '\\' && i + 1 < argString.length) { + i++ + current += argString[i] + } else { + current += ch + } + i++ + continue + } + + if (ch === '"' || ch === "'") { + quote = ch + i++ + continue + } + + if (/\s/.test(ch)) { + push() + i++ + while (i < argString.length && /\s/.test(argString[i])) i++ + continue + } + + current += ch + i++ + } + + push() + return tokens +} + +function parseBackportCommand(body) { + const cleaned = stripNoise(body) + const match = cleaned.match(/^@projt-launcher-bot\s+backport\b(.*)$/im) + if (!match) return null + + const tokens = tokenize(match[1] ?? '') + const targets = [] + const options = { + force: false, + noPr: false, + } + + for (let idx = 0; idx < tokens.length; idx++) { + const t = tokens[idx] + if (!t) continue + + if (t === '--force') { + options.force = true + continue + } + + if (t === '--no-pr') { + options.noPr = true + continue + } + + if (t === '--to') { + const next = tokens[idx + 1] + if (next) { + targets.push(next) + idx++ + } + continue + } + + if (t.startsWith('--to=')) { + targets.push(t.slice('--to='.length)) + continue + } + + if (t.startsWith('-')) { + continue + } + + targets.push(t) + } + + return { targets, options } +} + +function parseReleaseVersionTuple(branch) { + const m = String(branch).match(/^release-(v?\d+(?:\.\d+){1,2})(?:$|[-_].*)$/i) + if (!m) return null + const parts = m[1].replace(/^v/i, '').split('.').map((p) => Number(p)) + while (parts.length < 3) parts.push(0) + if (parts.some((n) => Number.isNaN(n))) return null + return parts +} + +function compareVersionTuples(a, b) { + for (let i = 0; i < Math.max(a.length, b.length); i++) { + const av = a[i] ?? 0 + const bv = b[i] ?? 0 + if (av !== bv) return av - bv + } + return 0 +} + +async function addReaction({ github, node_id, reaction }) { + await github.graphql( + `mutation($node_id: ID!, $reaction: ReactionContent!) { + addReaction(input: { content: $reaction, subjectId: $node_id }) { + clientMutationId + } + }`, + { node_id, reaction }, + ) +} + +async function listReleaseBranches({ github, context }) { + const branches = await github.paginate(github.rest.repos.listBranches, { + ...context.repo, + per_page: 100, + }) + return branches.map((b) => b.name).filter((n) => /^release-/.test(n)) +} + +async function resolveTargets({ github, context, core, pull_request, requestedTargets }) { + const releaseBranches = await listReleaseBranches({ github, context }) + const releaseSet = new Set(releaseBranches) + + const normalized = (requestedTargets ?? []) + .map((t) => String(t).trim()) + .filter(Boolean) + + const wantsAll = normalized.includes('all') + const wantsLatest = normalized.includes('latest') + + const explicit = normalized.filter((t) => t !== 'all' && t !== 'latest') + + const resolved = new Set() + + if (wantsAll) { + for (const b of releaseBranches) resolved.add(b) + } + + if (wantsLatest) { + const candidates = releaseBranches + .map((b) => ({ b, v: parseReleaseVersionTuple(b) })) + .filter((x) => x.v) + .sort((x, y) => compareVersionTuples(x.v, y.v)) + + if (candidates.length > 0) { + resolved.add(candidates[candidates.length - 1].b) + } else { + core.warning('No versioned release-* branches found for target "latest"') + } + } + + for (const t of explicit) { + if (releaseSet.has(t)) { + resolved.add(t) + } else { + core.warning(`Ignoring unknown target branch: ${t}`) + } + } + + // Fallback to PR labels if comment had no targets. + if (resolved.size === 0) { + const labels = (pull_request.labels ?? []).map((l) => l.name) + const labelTargets = [] + for (const label of labels) { + if (!label.startsWith('backport/')) continue + labelTargets.push(label.slice('backport/'.length)) + } + if (labelTargets.length > 0) { + return resolveTargets({ + github, + context, + core, + pull_request, + requestedTargets: labelTargets, + }) + } + } + + return [...resolved] +} + +async function git(args, opts = {}) { + const { cwd, core, allowFailure } = opts + try { + const { stdout, stderr } = await execFileAsync('git', args, { cwd }) + if (stderr && core) core.info(stderr.trim()) + return stdout.trim() + } catch (e) { + if (allowFailure) return null + throw e + } +} + +async function remoteBranchExists({ cwd, branch }) { + try { + await execFileAsync('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch], { cwd }) + return true + } catch { + return false + } +} + +async function getCommitParentCount({ cwd, sha }) { + const raw = await git(['cat-file', '-p', sha], { cwd }) + return raw.split('\n').filter((l) => l.startsWith('parent ')).length +} + +async function createOrReuseBackportPR({ + github, + context, + core, + targetBranch, + backportBranch, + originalPR, + originalTitle, + cherryPickedSha, + requestedVia = 'bot comment', +}) { + const head = `${context.repo.owner}:${backportBranch}` + + const { data: prs } = await github.rest.pulls.list({ + ...context.repo, + state: 'all', + head, + base: targetBranch, + per_page: 10, + }) + + if (prs.length > 0) { + return { number: prs[0].number, url: prs[0].html_url, state: prs[0].state, reused: true } + } + + const { data: created } = await github.rest.pulls.create({ + ...context.repo, + title: `[Backport ${targetBranch}] ${originalTitle}`, + body: [ + `Automated backport of #${originalPR} to \`${targetBranch}\`.`, + ``, + `- Original PR: #${originalPR}`, + `- Cherry-picked: \`${cherryPickedSha}\``, + `- Requested via ${requestedVia}`, + ].join('\n'), + head: backportBranch, + base: targetBranch, + maintainer_can_modify: true, + }) + + try { + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: created.number, + labels: ['automated-backport'], + }) + } catch (e) { + core.warning(`Failed to add label "automated-backport" to #${created.number}: ${e.message}`) + } + + return { number: created.number, url: created.html_url, state: created.state, reused: false } +} + +async function performBackport({ + github, + context, + core, + cwd, + pull_request, + targetBranch, + backportBranch, + mergeSha, + options, + requestedVia, +}) { + const baseBranch = pull_request.base.ref + + if (!options.force) { + const exists = await remoteBranchExists({ cwd, branch: backportBranch }) + if (exists) { + return { + targetBranch, + backportBranch, + status: 'skipped', + message: `Branch \`${backportBranch}\` already exists (use \`--force\` to rewrite)`, + } + } + } + + await git(['config', 'user.name', 'github-actions[bot]'], { cwd }) + await git(['config', 'user.email', 'github-actions[bot]@users.noreply.github.com'], { cwd }) + + await git(['fetch', 'origin', targetBranch, baseBranch], { cwd }) + await git(['checkout', '-B', backportBranch, `origin/${targetBranch}`], { cwd }) + + const parentCount = await getCommitParentCount({ cwd, sha: mergeSha }) + const cherryPickArgs = parentCount > 1 ? ['cherry-pick', '-m', '1', mergeSha] : ['cherry-pick', mergeSha] + + try { + await git(cherryPickArgs, { cwd }) + } catch (e) { + await git(['cherry-pick', '--abort'], { cwd, allowFailure: true }) + return { + targetBranch, + backportBranch, + status: 'conflict', + message: `Cherry-pick failed with conflicts for \`${targetBranch}\``, + } + } + + await git(['push', '--force-with-lease', 'origin', backportBranch], { cwd }) + + if (options.noPr) { + return { + targetBranch, + backportBranch, + status: 'pushed', + message: `Pushed \`${backportBranch}\` (PR creation disabled via --no-pr)`, + } + } + + const pr = await createOrReuseBackportPR({ + github, + context, + core, + targetBranch, + backportBranch, + originalPR: pull_request.number, + originalTitle: pull_request.title, + cherryPickedSha: mergeSha, + requestedVia, + }) + + return { + targetBranch, + backportBranch, + status: 'pr', + pr, + message: pr.reused + ? `Reused backport PR #${pr.number} (${pr.url})` + : `Created backport PR #${pr.number} (${pr.url})`, + } +} + +async function handleBackportComment({ github, context, core }) { + const payload = context.payload + const commentBody = payload.comment?.body ?? '' + const command = parseBackportCommand(commentBody) + if (!command) return false + + if (!payload.issue?.pull_request) { + core.info('Backport command ignored: not a pull request') + return false + } + + const association = payload.comment?.author_association + const allowed = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']) + if (!allowed.has(String(association))) { + core.info(`Backport command ignored: insufficient permissions (${association})`) + return false + } + + const prNumber = payload.issue.number + const { data: pull_request } = await github.rest.pulls.get({ + ...context.repo, + pull_number: prNumber, + }) + + if (!pull_request.merged) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: 'Backport request ignored: PR is not merged.', + }) + return true + } + + const nodeId = payload.comment?.node_id + if (nodeId) { + try { + await addReaction({ github, node_id: nodeId, reaction: 'EYES' }) + } catch { + // ignore + } + } + + const targets = await resolveTargets({ + github, + context, + core, + pull_request, + requestedTargets: command.targets, + }) + + if (targets.length === 0) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: [ + 'Backport failed: no valid targets resolved.', + '', + 'Use one of:', + '- `@projt-launcher-bot backport latest`', + '- `@projt-launcher-bot backport all`', + '- `@projt-launcher-bot backport release-1.2.3`', + ].join('\n'), + }) + if (nodeId) { + try { + await addReaction({ github, node_id: nodeId, reaction: 'CONFUSED' }) + } catch { + // ignore + } + } + return true + } + + const cwd = process.env.GITHUB_WORKSPACE || process.cwd() + const mergeSha = pull_request.merge_commit_sha + if (!mergeSha) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: 'Backport failed: merge commit SHA is missing for this PR.', + }) + if (nodeId) { + try { + await addReaction({ github, node_id: nodeId, reaction: 'CONFUSED' }) + } catch { + // ignore + } + } + return true + } + + const results = [] + for (const targetBranch of targets) { + const backportBranch = `backport/${targetBranch}/pr-${pull_request.number}` + const res = await performBackport({ + github, + context, + core, + cwd, + pull_request, + targetBranch, + backportBranch, + mergeSha, + options: command.options, + requestedVia: 'bot comment', + }) + results.push(res) + } + + const lines = [] + lines.push('## Backport results') + lines.push('') + lines.push(`Original PR: #${pull_request.number}`) + lines.push(`Cherry-picked: \`${mergeSha}\``) + lines.push('') + for (const r of results) { + if (r.status === 'pr') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'pushed') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'skipped') { + lines.push(`- SKIP \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'conflict') { + lines.push(`- FAIL \`${r.targetBranch}\`: ${r.message}`) + } else { + lines.push(`- WARN \`${r.targetBranch}\`: ${r.message ?? 'unknown status'}`) + } + } + + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: lines.join('\n'), + }) + + const anyConflict = results.some((r) => r.status === 'conflict') + if (nodeId) { + try { + await addReaction({ + github, + node_id: nodeId, + reaction: anyConflict ? 'CONFUSED' : 'ROCKET', + }) + } catch { + // ignore + } + } + + return true +} + +function getBackportLabelTargets(labels = []) { + return labels + .filter((l) => typeof l === 'string' && l.startsWith('backport/')) + .map((l) => l.slice('backport/'.length)) +} + +function optionsFromLabels(labelTargets = []) { + return { + force: labelTargets.includes('force'), + noPr: labelTargets.includes('no-pr'), + skip: labelTargets.includes('skip'), + } +} + +async function upsertBackportSummaryComment({ github, context, pull_number, body }) { + const marker = '' + const fullBody = [marker, body].join('\n') + + const comments = await github.paginate(github.rest.issues.listComments, { + ...context.repo, + issue_number: pull_number, + per_page: 100, + }) + + const existing = comments.find( + (c) => c.user?.login === 'github-actions[bot]' && typeof c.body === 'string' && c.body.includes(marker), + ) + + if (existing) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: existing.id, + body: fullBody, + }) + } else { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pull_number, + body: fullBody, + }) + } +} + +async function handleBackportOnClose({ github, context, core }) { + const payload = context.payload + const pr = payload.pull_request + if (!pr) return false + + // Only act when a PR is merged and has backport/* labels. + if (!pr.merged) return false + + const labelNames = (pr.labels ?? []).map((l) => l.name) + const labelTargets = getBackportLabelTargets(labelNames) + if (labelTargets.length === 0) return false + + const opts = optionsFromLabels(labelTargets) + if (opts.skip) { + core.info('Backport skipped via backport/skip label') + return true + } + + const requestedTargets = labelTargets.filter((t) => !['force', 'no-pr', 'skip'].includes(t)) + + const targets = await resolveTargets({ + github, + context, + core, + pull_request: pr, + requestedTargets, + }) + + if (targets.length === 0) { + await upsertBackportSummaryComment({ + github, + context, + pull_number: pr.number, + body: [ + '## Backport results', + '', + 'No valid targets resolved from backport labels.', + '', + `Labels: ${labelNames.filter((n) => n.startsWith('backport/')).join(', ')}`, + ].join('\n'), + }) + return true + } + + const mergeSha = pr.merge_commit_sha + if (!mergeSha) { + await upsertBackportSummaryComment({ + github, + context, + pull_number: pr.number, + body: 'Backport failed: merge commit SHA is missing for this PR.', + }) + return true + } + + const cwd = process.env.GITHUB_WORKSPACE || process.cwd() + const results = [] + for (const targetBranch of targets) { + const backportBranch = `backport/${targetBranch}/pr-${pr.number}` + const res = await performBackport({ + github, + context, + core, + cwd, + pull_request: pr, + targetBranch, + backportBranch, + mergeSha, + options: { force: opts.force, noPr: opts.noPr }, + requestedVia: 'labels', + }) + results.push(res) + } + + const lines = [] + lines.push('## Backport results') + lines.push('') + lines.push(`Original PR: #${pr.number}`) + lines.push(`Cherry-picked: \`${mergeSha}\``) + lines.push('') + for (const r of results) { + if (r.status === 'pr') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'pushed') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'skipped') { + lines.push(`- SKIP \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'conflict') { + lines.push(`- FAIL \`${r.targetBranch}\`: ${r.message}`) + } else { + lines.push(`- WARN \`${r.targetBranch}\`: ${r.message ?? 'unknown status'}`) + } + } + + await upsertBackportSummaryComment({ + github, + context, + pull_number: pr.number, + body: lines.join('\n'), + }) + + return true +} + +module.exports = { + parseBackportCommand, + handleBackportComment, + handleBackportOnClose, +} diff --git a/archived/projt-launcher/ci/github-script/commit-types.json b/archived/projt-launcher/ci/github-script/commit-types.json new file mode 100644 index 0000000000..e6b206b1bf --- /dev/null +++ b/archived/projt-launcher/ci/github-script/commit-types.json @@ -0,0 +1,70 @@ +{ + "types": [ + "deps", + "dependencies", + "dep", + "upgrade", + "downgrade", + "bump", + "release", + "hotfix", + "security", + "vulnerability", + "localization", + "translation", + "i18n", + "l10n", + "internationalization", + "localisation", + "config", + "configuration", + "cleanup", + "clean", + "maintenance", + "infra", + "infrastructure", + "ops", + "operations", + "devops", + "qa", + "ux", + "ui", + "api", + "backend", + "frontend", + "data", + "database", + "schema", + "samples", + "examples", + "assets", + "content", + "docs-build", + "docs-ci", + "docs-config", + "docs-deps", + "docs-release", + "meta", + "init", + "prototype", + "experiment", + "hotpath", + "breaking", + "deprecate", + "compat", + "migration", + "interop", + "benchmark", + "profiles", + "telemetry", + "analytics", + "observability", + "state", + "sync", + "validation", + "lint", + "formatter", + "package", + "vendor" + ] +} diff --git a/archived/projt-launcher/ci/github-script/commits.js b/archived/projt-launcher/ci/github-script/commits.js new file mode 100644 index 0000000000..27dbd61eb6 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/commits.js @@ -0,0 +1,336 @@ +/** + * ProjT Launcher - Commit Validation for Pull Requests + * Validates commit messages, structure, and conventions + */ + +const { classify } = require('../supportedBranches.js') +const withRateLimit = require('./withRateLimit.js') +const { dismissReviews, postReview } = require('./reviews.js') + +const commitTypeConfig = (() => { + try { + return require('./commit-types.json') + } catch (error) { + console.warn(`commit validator: could not load commit-types.json (${error.message})`) + return {} + } +})() + +const parseCommitTypeList = (value) => { + if (!value) { + return [] + } + return value + .split(',') + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean) +} + +const DEFAULT_COMMIT_TYPES = [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', + 'deps', +] + +const EXTENDED_COMMIT_TYPES = [ + ...(commitTypeConfig.types ?? []), +] + +const ENV_COMMIT_TYPES = parseCommitTypeList( + process.env.COMMIT_TYPES ?? process.env.ADDITIONAL_COMMIT_TYPES ?? '' +) + +const COMMIT_TYPES = Array.from( + new Set([...DEFAULT_COMMIT_TYPES, ...EXTENDED_COMMIT_TYPES, ...ENV_COMMIT_TYPES]) +) + +const COMMIT_TYPE_SET = new Set(COMMIT_TYPES) + +// Component scopes for ProjT Launcher +const VALID_SCOPES = [ + 'core', + 'ui', + 'minecraft', + 'modplatform', + 'modrinth', + 'curseforge', + 'ftb', + 'technic', + 'atlauncher', + 'auth', + 'java', + 'news', + 'settings', + 'skins', + 'translations', + 'build', + 'ci', + 'nix', + 'vcpkg', + 'deps', +] + +/** + * Validate commit message format + * Expected format: type(scope): description + * @param {string} message - Commit message + * @returns {object} Validation result + */ +function normalizeCommitType(type) { + if (!type) { + return '' + } + const trimmed = type.toLowerCase() + const legacyMatch = trimmed.match(/^\d+\.(.+)$/) + return legacyMatch ? legacyMatch[1] : trimmed +} + +function validateCommitMessage(message) { + const firstLine = message.split('\n')[0] + + // Check for conventional commit format + const conventionalMatch = firstLine.match( + /^(?[\w.-]+)(?:\((?[\w-]+)\))?!?:\s*(?.+)$/ + ) + + if (!conventionalMatch) { + return { + valid: false, + severity: 'warning', + message: `Commit message doesn't follow conventional format: "${firstLine.substring(0, 50)}..."`, + } + } + + const { type, scope, description } = conventionalMatch.groups + const normalizedType = normalizeCommitType(type) + + // Validate type + if (!COMMIT_TYPE_SET.has(normalizedType)) { + return { + valid: false, + severity: 'warning', + message: `Unknown commit type "${type}". Valid types: ${COMMIT_TYPES.join(', ')}`, + } + } + + // Validate scope if present + if (scope && !VALID_SCOPES.includes(scope.toLowerCase())) { + return { + valid: false, + severity: 'info', + message: `Unknown scope "${scope}". Consider using: ${VALID_SCOPES.slice(0, 5).join(', ')}...`, + } + } + + // Check description length + if (description.length < 10) { + return { + valid: false, + severity: 'warning', + message: 'Commit description too short (min 10 chars)', + } + } + + if (firstLine.length > 140) { + return { + valid: false, + severity: 'info', + message: 'First line exceeds 140 characters', + } + } + + return { valid: true } +} + +/** + * Check commit for specific patterns + * @param {object} commit - Commit object + * @returns {object} Check result + */ +function checkCommitPatterns(commit) { + const message = commit.message + const issues = [] + + // Check for WIP markers + if (message.match(/\bWIP\b/i)) { + issues.push({ + severity: 'warning', + message: 'Commit contains WIP marker', + }) + } + + // Check for fixup/squash commits + if (message.match(/^(fixup|squash)!/i)) { + issues.push({ + severity: 'info', + message: 'Commit is a fixup/squash commit - remember to rebase before merge', + }) + } + + // Check for merge commits + if (message.startsWith('Merge ')) { + issues.push({ + severity: 'info', + message: 'Merge commit detected - consider rebasing instead', + }) + } + + // Check for large descriptions without body + if (message.split('\n').length === 1 && message.length > 100) { + issues.push({ + severity: 'info', + message: 'Long commit message without body - consider adding details in commit body', + }) + } + + return issues +} + +/** + * Validate all commits in a PR + */ +async function run({ github, context, core, dry }) { + await withRateLimit({ github, core }, async (stats) => { + stats.prs = 1 + + const pull_number = context.payload.pull_request.number + const base = context.payload.pull_request.base.ref + const baseClassification = classify(base) + + // Get all commits in the PR + const commits = await github.paginate(github.rest.pulls.listCommits, { + ...context.repo, + pull_number, + }) + + core.info(`Validating ${commits.length} commits for PR #${pull_number}`) + + const results = [] + + for (const { sha, commit } of commits) { + const commitResults = { + sha: sha.substring(0, 7), + fullSha: sha, + author: commit.author.name, + message: commit.message.split('\n')[0], + issues: [], + } + + // Validate commit message format + const formatValidation = validateCommitMessage(commit.message) + if (!formatValidation.valid) { + commitResults.issues.push({ + severity: formatValidation.severity, + message: formatValidation.message, + }) + } + + // Check for commit patterns + const patternIssues = checkCommitPatterns(commit) + commitResults.issues.push(...patternIssues) + + results.push(commitResults) + } + + // Log results + let hasErrors = false + let hasWarnings = false + + for (const result of results) { + core.startGroup(`Commit ${result.sha}`) + core.info(`Author: ${result.author}`) + core.info(`Message: ${result.message}`) + + if (result.issues.length === 0) { + core.info('✓ No issues found') + } else { + for (const issue of result.issues) { + switch (issue.severity) { + case 'error': + core.error(issue.message) + hasErrors = true + break + case 'warning': + core.warning(issue.message) + hasWarnings = true + break + default: + core.info(`ℹ ${issue.message}`) + } + } + } + core.endGroup() + } + + // If all commits are valid, dismiss any previous reviews + if (!hasErrors && !hasWarnings) { + await dismissReviews({ github, context, dry }) + core.info('✓ All commits passed validation') + return + } + + // Generate summary for issues + const issueCommits = results.filter(r => r.issues.length > 0) + + if (issueCommits.length > 0) { + const body = [ + '## Commit Validation Issues', + '', + 'The following commits have issues that should be addressed:', + '', + ...issueCommits.flatMap(commit => [ + `### \`${commit.sha}\`: ${commit.message}`, + '', + ...commit.issues.map(issue => `- **${issue.severity}**: ${issue.message}`), + '', + ]), + '---', + '', + '### Commit Message Guidelines', + '', + 'ProjT Launcher uses [Conventional Commits](https://www.conventionalcommits.org/):', + '', + '```', + 'type(scope): description', + '', + '[optional body]', + '', + '[optional footer]', + '```', + '', + `**Types**: ${COMMIT_TYPES.join(', ')}`, + '', + `**Scopes**: ${VALID_SCOPES.slice(0, 8).join(', ')}, ...`, + ].join('\n') + + // Post review only for errors/warnings, not info + if (hasErrors || hasWarnings) { + await postReview({ github, context, core, dry, body }) + } + + // Write step summary + const fs = require('node:fs') + if (process.env.GITHUB_STEP_SUMMARY) { + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, body) + } + } + + if (hasErrors) { + throw new Error('Commit validation failed with errors') + } + }) +} + +module.exports = run +module.exports.validateCommitMessage = validateCommitMessage +module.exports.checkCommitPatterns = checkCommitPatterns +module.exports.normalizeCommitType = normalizeCommitType diff --git a/archived/projt-launcher/ci/github-script/get-teams.js b/archived/projt-launcher/ci/github-script/get-teams.js new file mode 100644 index 0000000000..c547d5ac62 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/get-teams.js @@ -0,0 +1,134 @@ +/** + * ProjT Launcher - Team Information Fetcher + * Fetches team information from GitHub organization for CI purposes + */ + +// Teams to exclude from processing (bots, voters, etc.) +const excludeTeams = [ + /^voters.*$/, + /^bots?$/, +] + +/** + * Main function to fetch team information + */ +module.exports = async ({ github, context, core, outFile }) => { + const withRateLimit = require('./withRateLimit.js') + const { writeFileSync } = require('node:fs') + + const org = context.repo.owner + const result = {} + + await withRateLimit({ github, core }, async () => { + /** + * Convert array of users to object mapping login -> id + */ + function makeUserSet(users) { + users.sort((a, b) => (a.login > b.login ? 1 : -1)) + return users.reduce((acc, user) => { + acc[user.login] = user.id + return acc + }, {}) + } + + /** + * Process teams recursively + */ + async function processTeams(teams) { + for (const team of teams) { + // Skip excluded teams + if (excludeTeams.some((regex) => team.slug.match(regex))) { + core.info(`Skipping excluded team: ${team.slug}`) + continue + } + + core.notice(`Processing team ${team.slug}`) + + try { + // Get team members + const members = makeUserSet( + await github.paginate(github.rest.teams.listMembersInOrg, { + org, + team_slug: team.slug, + role: 'member', + }), + ) + + // Get team maintainers + const maintainers = makeUserSet( + await github.paginate(github.rest.teams.listMembersInOrg, { + org, + team_slug: team.slug, + role: 'maintainer', + }), + ) + + result[team.slug] = { + description: team.description, + id: team.id, + maintainers, + members, + name: team.name, + } + } catch (e) { + core.warning(`Failed to fetch team ${team.slug}: ${e.message}`) + } + + // Process child teams + try { + const childTeams = await github.paginate( + github.rest.teams.listChildInOrg, + { + org, + team_slug: team.slug, + }, + ) + await processTeams(childTeams) + } catch (e) { + // Child teams might not exist or be accessible + core.info(`No child teams for ${team.slug}`) + } + } + } + + // Get all teams with access to the repository + try { + const teams = await github.paginate(github.rest.repos.listTeams, { + ...context.repo, + }) + + core.info(`Found ${teams.length} teams with repository access`) + await processTeams(teams) + } catch (e) { + core.warning(`Could not fetch repository teams: ${e.message}`) + + // Fallback: create minimal team structure + result['projt-maintainers'] = { + description: 'ProjT Launcher Maintainers', + id: 0, + maintainers: {}, + members: {}, + name: 'ProjT Maintainers', + } + } + }) + + // Sort teams alphabetically + const sorted = Object.keys(result) + .sort() + .reduce((acc, key) => { + acc[key] = result[key] + return acc + }, {}) + + const json = `${JSON.stringify(sorted, null, 2)}\n` + + if (outFile) { + writeFileSync(outFile, json) + core.info(`Team information written to ${outFile}`) + } else { + console.log(json) + } + + return sorted +} diff --git a/archived/projt-launcher/ci/github-script/merge.js b/archived/projt-launcher/ci/github-script/merge.js new file mode 100644 index 0000000000..536af0f056 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/merge.js @@ -0,0 +1,308 @@ +/** + * ProjT Launcher - Merge Handler + * Handles PR merge operations with validation and queue management + */ + +const { classify } = require('../supportedBranches.js') + +// Component definitions for ProjT Launcher +const COMPONENTS = { + core: ['launcher/', 'systeminfo/', 'katabasis/', 'libnbtplusplus/', 'launcherjava/'], + ui: ['launcher/ui/', 'launcher/resources/', 'launcher/ui/'], + minecraft: ['launcher/minecraft/', 'tomlplusplus/', 'qdcss/'], + modplatform: ['launcher/modplatform/'], + build: ['CMakeLists.txt', 'cmake/', 'vcpkg.json', 'CMakePresets.json'], + docs: ['docs/', 'README.md', 'CONTRIBUTING.md'], + ci: ['.github/', 'ci/'], +} + +/** + * Get component owners for changed files + * @param {Array} files - Changed files + * @returns {Set} Component owners + */ +function getComponentOwners(files) { + const owners = new Set() + + for (const { filename } of files) { + for (const [component, paths] of Object.entries(COMPONENTS)) { + if (paths.some(path => filename.startsWith(path) || filename === path)) { + owners.add(component) + } + } + } + + return owners +} + +/** + * Run merge checklist for ProjT Launcher PRs + */ +function runChecklist({ + committers, + events, + files, + pull_request, + log, + maintainers, + user, + userIsMaintainer, +}) { + // Check what components are touched + const components = getComponentOwners(files) + + // Get eligible reviewers from maintainers + const eligible = maintainers && maintainers.length > 0 + ? new Set(maintainers) + : new Set() + + // Get current approvals + const approvals = new Set( + events + .filter( + ({ event, state, commit_id }) => + event === 'reviewed' && + state === 'approved' && + // Only approvals for the current head SHA count + commit_id === pull_request.head.sha, + ) + .map(({ user }) => user?.id) + .filter(Boolean), + ) + + const checklist = { + 'PR targets a development branch (develop, master)': + classify(pull_request.base.ref).type.includes('development'), + + 'PR has passing CI checks': + pull_request.mergeable_state !== 'blocked', + + 'PR is at least one of:': { + 'Approved by a maintainer': committers.intersection(approvals).size > 0, + 'Opened by a maintainer': committers.has(pull_request.user.id), + 'Part of a backport': + pull_request.head.ref.startsWith('backport-') || + pull_request.labels?.some(l => l.name === 'backport'), + }, + + 'PR has no merge conflicts': + pull_request.mergeable === true, + } + + if (user) { + checklist[`${user.login} is a project maintainer`] = userIsMaintainer + if (components.size > 0) { + checklist[`${user.login} owns touched components (${Array.from(components).join(', ')})`] = + eligible.has(user.id) + } + } else { + checklist['PR has eligible reviewers'] = eligible.size > 0 + } + + const result = Object.values(checklist).every((v) => + typeof v === 'boolean' ? v : Object.values(v).some(Boolean), + ) + + log('checklist', JSON.stringify(checklist)) + log('components', JSON.stringify(Array.from(components))) + log('eligible', JSON.stringify(Array.from(eligible))) + log('result', result) + + return { + checklist, + eligible, + components, + result, + } +} + +/** + * Check for merge command in comment + * Format: @projt-launcher-bot merge + */ +function hasMergeCommand(body) { + return (body ?? '') + .replace(//gms, '') + .replace(/(^`{3,})[^`].*?\1/gms, '') + .match(/^@projt-launcher-bot\s+merge\s*$/im) +} + +/** + * Handle merge comment reaction + */ +async function handleMergeComment({ github, body, node_id, reaction }) { + if (!hasMergeCommand(body)) return + + await github.graphql( + `mutation($node_id: ID!, $reaction: ReactionContent!) { + addReaction(input: { + content: $reaction, + subjectId: $node_id + }) + { clientMutationId } + }`, + { node_id, reaction }, + ) +} + +/** + * Handle merge request for a PR + */ +async function handleMerge({ + github, + context, + core, + log, + dry, + pull_request, + events, + maintainers, + getTeamMembers, + getUser, +}) { + const pull_number = pull_request.number + + // Get list of maintainers (project committers) + const committers = new Set( + (await getTeamMembers('projt-maintainers')).map(({ id }) => id), + ) + + // Get changed files + const files = ( + await github.rest.pulls.listFiles({ + ...context.repo, + pull_number, + per_page: 100, + }) + ).data + + // Early exit for large PRs + if (files.length >= 100) { + core.warning('PR touches 100+ files, manual merge required') + return false + } + + // Only look through comments after the latest push + const lastPush = events.findLastIndex( + ({ event, sha, commit_id }) => + ['committed', 'head_ref_force_pushed'].includes(event) && + (sha ?? commit_id) === pull_request.head.sha, + ) + + const comments = events.slice(lastPush + 1).filter( + ({ event, body, user, node_id }) => + ['commented', 'reviewed'].includes(event) && + hasMergeCommand(body) && + user && + (dry || + !events.some( + ({ event, body }) => + ['commented'].includes(event) && + body.match(new RegExp(`^$`, 'm')), + )), + ) + + /** + * Perform the merge + */ + async function merge() { + if (dry) { + core.info(`Would merge #${pull_number}... (dry run)`) + return 'Merge completed (dry run)' + } + + // Use merge queue if available, otherwise regular merge + try { + const resp = await github.graphql( + `mutation($node_id: ID!, $sha: GitObjectID) { + enqueuePullRequest(input: { + expectedHeadOid: $sha, + pullRequestId: $node_id + }) + { + clientMutationId, + mergeQueueEntry { mergeQueue { url } } + } + }`, + { node_id: pull_request.node_id, sha: pull_request.head.sha }, + ) + return [ + `:heavy_check_mark: [Queued](${resp.enqueuePullRequest.mergeQueueEntry.mergeQueue.url}) for merge`, + ] + } catch (e) { + log('Queue merge failed, trying direct merge', e.response?.errors?.[0]?.message) + } + + // Fallback to direct merge + try { + await github.rest.pulls.merge({ + ...context.repo, + pull_number, + merge_method: 'squash', + sha: pull_request.head.sha, + }) + return [':heavy_check_mark: Merged successfully'] + } catch (e) { + return [`:x: Merge failed: ${e.message}`] + } + } + + // Process merge commands + for (const comment of comments) { + const user = await getUser(comment.user.id) + + const { checklist, result } = runChecklist({ + committers, + events, + files, + pull_request, + log, + maintainers: maintainers || [], + user, + userIsMaintainer: committers.has(user.id), + }) + + const response = [] + + if (result) { + response.push(...(await merge())) + } else { + response.push(':x: Cannot merge - checklist not satisfied:') + response.push('') + response.push('```') + response.push(JSON.stringify(checklist, null, 2)) + response.push('```') + } + + if (!dry) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pull_number, + body: [ + ``, + '', + ...response, + ].join('\n'), + }) + + await handleMergeComment({ + github, + body: comment.body, + node_id: comment.node_id, + reaction: result ? 'ROCKET' : 'CONFUSED', + }) + } else { + core.info(`Response: ${response.join('\n')}`) + } + } + + return comments.length > 0 +} + +module.exports = { + runChecklist, + hasMergeCommand, + handleMergeComment, + handleMerge, + getComponentOwners, +} diff --git a/archived/projt-launcher/ci/github-script/package-lock.json b/archived/projt-launcher/ci/github-script/package-lock.json new file mode 100644 index 0000000000..62a633e0d6 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/package-lock.json @@ -0,0 +1,1721 @@ +{ + "name": "github-script", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "version": "1.0.0", + "dependencies": { + "@actions/artifact": "6.1.0", + "@actions/core": "3.0.0", + "@actions/github": "9.0.0", + "bottleneck": "2.19.5", + "commander": "14.0.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@actions/artifact": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-6.1.0.tgz", + "integrity": "sha512-oRn9YhKkboXgIq2TQZ9uj6bhkT5ZUzFtnyTQ0tLGBwImaD0GfWShE5R0tPbN25EJmS3tz5sDd2JnVokAOtNrZQ==", + "license": "MIT", + "dependencies": { + "@actions/core": "^3.0.0", + "@actions/github": "^9.0.0", + "@actions/http-client": "^4.0.0", + "@azure/storage-blob": "^12.30.0", + "@octokit/core": "^7.0.6", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-retry": "^8.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "@protobuf-ts/plugin": "^2.2.3-alpha.1", + "@protobuf-ts/runtime": "^2.9.4", + "archiver": "^7.0.1", + "jwt-decode": "^4.0.0", + "unzip-stream": "^0.3.1" + } + }, + "node_modules/@actions/artifact/node_modules/@actions/http-client": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" + } + }, + "node_modules/@actions/core/node_modules/@actions/http-client": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/exec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", + "license": "MIT", + "dependencies": { + "@actions/io": "^3.0.2" + } + }, + "node_modules/@actions/github": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.0.0.tgz", + "integrity": "sha512-yJ0RoswsAaKcvkmpCE4XxBRiy/whH2SdTBHWzs0gi4wkqTDhXMChjSdqBz/F4AeiDlP28rQqL33iHb+kjAMX6w==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^3.0.2", + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@actions/http-client": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz", + "integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", + "license": "MIT" + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz", + "integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.30.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.30.0.tgz", + "integrity": "sha512-peDCR8blSqhsAKDbpSP/o55S4sheNwSrblvCaHUZ5xUI73XA7ieUGGwrONgD/Fng0EoDe1VOa3fAQ7+WGB3Ocg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.2.0", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.2.0.tgz", + "integrity": "sha512-YZLxiJ3vBAAnFbG3TFuAMUlxZRexjQX5JDQxOkFGb6e2TpoxH3xyHI6idsMe/QrWtj41U/KoqBxlayzhS+LlwA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.0.tgz", + "integrity": "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.6.0.tgz", + "integrity": "sha512-mfAwI+4GqUtbw/ddfyolEHaAL86ozRIVlOg2A+SVRbjx1CjsMc1YJO+hBSkt/pqfpR+PmWBbZLstHbXP8KGtMQ==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.6.0", + "@typescript/vfs": "^1.5.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz", + "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.3.tgz", + "integrity": "sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=7" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz", + "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.2", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobuf-ts/plugin": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.11.1.tgz", + "integrity": "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^2.4.0", + "@bufbuild/protoplugin": "^2.4.0", + "@protobuf-ts/protoc": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "typescript": "^3.9" + }, + "bin": { + "protoc-gen-dump": "bin/protoc-gen-dump", + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/@protobuf-ts/protoc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz", + "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==", + "license": "Apache-2.0", + "bin": { + "protoc": "protoc.js" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "node_modules/@typescript/vfs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.1.tgz", + "integrity": "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/unzip-stream": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz", + "integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==", + "license": "MIT", + "dependencies": { + "binary": "^0.3.0", + "mkdirp": "^0.5.1" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/archived/projt-launcher/ci/github-script/package.json b/archived/projt-launcher/ci/github-script/package.json new file mode 100644 index 0000000000..e4263e2ddf --- /dev/null +++ b/archived/projt-launcher/ci/github-script/package.json @@ -0,0 +1,19 @@ +{ + "name": "projt-launcher-github-scripts", + "version": "1.0.0", + "description": "GitHub Actions scripts for ProjT Launcher CI", + "private": true, + "scripts": { + "test": "node test/commits.test.js" + }, + "dependencies": { + "@actions/artifact": "6.1.0", + "@actions/core": "3.0.0", + "@actions/github": "9.0.0", + "bottleneck": "2.19.5", + "commander": "14.0.3" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/archived/projt-launcher/ci/github-script/prepare.js b/archived/projt-launcher/ci/github-script/prepare.js new file mode 100644 index 0000000000..2c60314f11 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/prepare.js @@ -0,0 +1,314 @@ +/** + * ProjT Launcher - PR Preparation Script + * Validates PR structure and prepares merge information + */ + +const { classify } = require('../supportedBranches.js') +const { postReview } = require('./reviews.js') + +const SIGNOFF_MARKER = '' + +function stripNoise(body = '') { + return String(body) + .replace(/\r/g, '') + .replace(//gms, '') + .replace(/(^`{3,})[^`].*?\1/gms, '') +} + +function hasSignedOffBy(body = '') { + const cleaned = stripNoise(body) + return /^signed-off-by:\s+.+<[^<>]+>\s*$/im.test(cleaned) +} + +async function dismissSignoffReviews({ github, context, pull_number }) { + const reviews = await github.paginate(github.rest.pulls.listReviews, { + ...context.repo, + pull_number, + }) + + const signoffReviews = reviews.filter( + (r) => + r.user?.login === 'github-actions[bot]' && + r.state === 'CHANGES_REQUESTED' && + typeof r.body === 'string' && + r.body.includes(SIGNOFF_MARKER), + ) + + for (const review of signoffReviews) { + await github.rest.pulls.dismissReview({ + ...context.repo, + pull_number, + review_id: review.id, + message: 'Signed-off-by found, thank you!', + }) + } +} + +/** + * Main PR preparation function + * Validates that the PR targets the correct branch and can be merged + */ +module.exports = async ({ github, context, core, dry }) => { + const payload = context.payload || {} + const pull_number = + payload?.pull_request?.number ?? + (Array.isArray(payload?.merge_group?.pull_requests) && + payload.merge_group.pull_requests[0]?.number) + + if (typeof pull_number !== 'number') { + core.info('No pull request found on this event; skipping prepare step.') + return { ok: true, skipped: true, reason: 'no-pull-request' } + } + + // Wait for GitHub to compute merge status + for (const retryInterval of [5, 10, 20, 40]) { + core.info('Checking whether the pull request can be merged...') + const prInfo = ( + await github.rest.pulls.get({ + ...context.repo, + pull_number, + }) + ).data + + if (prInfo.state !== 'open') { + throw new Error('PR is not open anymore.') + } + + if (prInfo.mergeable == null) { + core.info( + `GitHub is still computing merge status, waiting ${retryInterval} seconds...`, + ) + await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000)) + continue + } + + const { base, head, user } = prInfo + + const authorLogin = user?.login ?? '' + const isBotAuthor = + (user?.type ?? '').toLowerCase() === 'bot' || /\[bot\]$/i.test(authorLogin) + + // Enforce PR template sign-off (Signed-off-by: Name ) + if (isBotAuthor) { + core.info(`Skipping Signed-off-by requirement for bot author: ${authorLogin}`) + if (!dry) { + await dismissSignoffReviews({ github, context, pull_number }) + } + } else if (!hasSignedOffBy(prInfo.body)) { + const body = [ + SIGNOFF_MARKER, + '', + '## Missing Signed-off-by', + '', + 'This repository requires a DCO-style sign-off line in the PR description.', + '', + 'Add a line like this to the PR description (under “Signed-off-byâ€):', + '', + '```', + 'Signed-off-by: Your Name ', + '```', + '', + 'After updating the PR description, this check will re-run automatically.', + ].join('\n') + + await postReview({ github, context, core, dry, body }) + throw new Error('Missing Signed-off-by in PR description') + } else if (!dry) { + await dismissSignoffReviews({ github, context, pull_number }) + } + + // Classify base branch + const baseClassification = classify(base.ref) + core.setOutput('base', baseClassification) + core.info(`Base branch classification: ${JSON.stringify(baseClassification)}`) + + // Classify head branch + const headClassification = + base.repo.full_name === head.repo.full_name + ? classify(head.ref) + : { type: ['wip'] } // PRs from forks are WIP + core.setOutput('head', headClassification) + core.info(`Head branch classification: ${JSON.stringify(headClassification)}`) + + // Validate base branch targeting + if (!baseClassification.type.includes('development') && + !baseClassification.type.includes('release')) { + const body = [ + '## Invalid Target Branch', + '', + `This PR targets \`${base.ref}\`, which is not a valid target branch.`, + '', + '### Valid target branches for ProjT Launcher:', + '', + '| Branch | Purpose |', + '|--------|---------|', + '| `develop` | Main development branch |', + '| `master` / `main` | Stable branch |', + '| `release-X.Y.Z` | Release branches |', + '', + 'Please [change the base branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request) to the appropriate target.', + ].join('\n') + + await postReview({ github, context, core, dry, body }) + throw new Error('PR targets invalid branch.') + } + + // Check for release branch targeting from wrong branch + if (baseClassification.isRelease) { + // For release branches, typically only hotfixes and backports should target them + const isBackport = head.ref.startsWith('backport-') + const isHotfix = head.ref.startsWith('hotfix-') || head.ref.startsWith('hotfix/') + + if (!isBackport && !isHotfix && headClassification.type.includes('wip')) { + const body = [ + '## Release Branch Warning', + '', + `This PR targets the release branch \`${base.ref}\`.`, + '', + 'Release branches should only receive:', + '- **Backports** from the development branch', + '- **Hotfixes** for critical bugs', + '', + 'If this is a regular feature/fix, please target `develop` instead.', + '', + 'If this is intentionally a hotfix, consider naming your branch `hotfix/description`.', + ].join('\n') + + await postReview({ github, context, core, dry, body }) + // This is a warning, not an error + core.warning('PR targets release branch from non-hotfix/backport branch') + } + } + + // Validate feature branches target develop + if (headClassification.isFeature && + !['develop'].includes(base.ref)) { + const body = [ + '## Feature Branch Target', + '', + `Feature branches should typically target \`develop\`, not \`${base.ref}\`.`, + '', + 'Please verify this is the correct target branch.', + ].join('\n') + + core.warning(body) + // Don't block, just warn + } + + // Process merge state + let mergedSha, targetSha + + if (prInfo.mergeable) { + core.info('✓ PR can be merged.') + + mergedSha = prInfo.merge_commit_sha + targetSha = ( + await github.rest.repos.getCommit({ + ...context.repo, + ref: prInfo.merge_commit_sha, + }) + ).data.parents[0].sha + } else { + core.warning('âš  PR has merge conflicts.') + + mergedSha = head.sha + targetSha = ( + await github.rest.repos.compareCommitsWithBasehead({ + ...context.repo, + basehead: `${base.sha}...${head.sha}`, + }) + ).data.merge_base_commit.sha + } + + // Set outputs for downstream jobs + core.setOutput('mergedSha', mergedSha) + core.setOutput('targetSha', targetSha) + core.setOutput('mergeable', prInfo.mergeable) + core.setOutput('headSha', head.sha) + core.setOutput('baseSha', base.sha) + + // Get changed files for analysis + const files = await github.paginate(github.rest.pulls.listFiles, { + ...context.repo, + pull_number, + per_page: 100, + }) + + // Categorize changes + const categories = { + source: files.filter(f => + f.filename.startsWith('launcher/') + ).length, + ui: files.filter(f => + f.filename.includes('/ui/') + ).length, + build: files.filter(f => + f.filename.includes('CMake') || + f.filename.includes('vcpkg') || + f.filename.endsWith('.cmake') + ).length, + ci: files.filter(f => + f.filename.startsWith('.github/') || + f.filename.startsWith('ci/') + ).length, + docs: files.filter(f => + f.filename.startsWith('docs/') || + f.filename.endsWith('.md') + ).length, + translations: files.filter(f => + f.filename.includes('translations/') + ).length, + } + + core.info(`Changes summary:`) + core.info(` Source files: ${categories.source}`) + core.info(` UI files: ${categories.ui}`) + core.info(` Build files: ${categories.build}`) + core.info(` CI files: ${categories.ci}`) + core.info(` Documentation: ${categories.docs}`) + core.info(` Translations: ${categories.translations}`) + + core.setOutput('categories', JSON.stringify(categories)) + core.setOutput('totalFiles', files.length) + + // Write step summary + if (process.env.GITHUB_STEP_SUMMARY) { + const fs = require('node:fs') + const summary = [ + '## PR Preparation Summary', + '', + `| Property | Value |`, + `|----------|-------|`, + `| PR Number | #${pull_number} |`, + `| Base Branch | \`${base.ref}\` |`, + `| Head Branch | \`${head.ref}\` |`, + `| Mergeable | ${prInfo.mergeable ? '✅ Yes' : '⌠No'} |`, + `| Total Files | ${files.length} |`, + '', + '### Change Categories', + '', + `| Category | Files |`, + `|----------|-------|`, + ...Object.entries(categories).map(([cat, count]) => + `| ${cat.charAt(0).toUpperCase() + cat.slice(1)} | ${count} |` + ), + ].join('\n') + + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary) + } + + return { + mergeable: prInfo.mergeable, + mergedSha, + targetSha, + headSha: head.sha, + baseSha: base.sha, + base: baseClassification, + head: headClassification, + files: files.length, + categories, + } + } + + throw new Error('Timeout waiting for merge status computation') +} diff --git a/archived/projt-launcher/ci/github-script/reviewers.js b/archived/projt-launcher/ci/github-script/reviewers.js new file mode 100644 index 0000000000..9a2fd8df6a --- /dev/null +++ b/archived/projt-launcher/ci/github-script/reviewers.js @@ -0,0 +1,329 @@ +/** + * ProjT Launcher - Reviewer Assignment + * Automatically assigns reviewers based on changed files and CODEOWNERS + */ + +const fs = require('node:fs') +const path = require('node:path') + +function extractMaintainersBlock(source) { + const token = 'maintainers =' + const start = source.indexOf(token) + if (start === -1) { + return '' + } + + const braceStart = source.indexOf('{', start) + if (braceStart === -1) { + return '' + } + + let depth = 0 + for (let i = braceStart; i < source.length; i += 1) { + const char = source[i] + if (char === '{') { + depth += 1 + } else if (char === '}') { + depth -= 1 + if (depth === 0) { + return source.slice(braceStart, i + 1) + } + } + } + return '' +} + +function parseAreas(areaBlock) { + const matches = areaBlock.match(/"([^"]+)"/g) || [] + return matches.map(entry => entry.replace(/"/g, '')) +} + +function loadMaintainersFromNix() { + const maintainersPath = path.join(__dirname, '..', 'eval', 'compare', 'maintainers.nix') + try { + const source = fs.readFileSync(maintainersPath, 'utf8') + const block = extractMaintainersBlock(source) + if (!block) { + return [] + } + + const entryRegex = /(\w+)\s*=\s*{([\s\S]*?)\n\s*};/g + const maintainers = [] + let match + while ((match = entryRegex.exec(block)) !== null) { + const [, , body] = match + const githubMatch = body.match(/github\s*=\s*"([^"]+)"/) + if (!githubMatch) continue + const areasMatch = body.match(/areas\s*=\s*\[([\s\S]*?)\]/) + const areas = areasMatch ? parseAreas(areasMatch[1]) : [] + maintainers.push({ + handle: githubMatch[1], + areas, + }) + } + return maintainers + } catch (error) { + console.warn(`Could not read maintainers from maintainers.nix: ${error.message}`) + return [] + } +} + +const FALLBACK_MAINTAINERS = [ + { + handle: 'YongDo-Hyun', + areas: ['all'], + }, + { + handle: 'grxtor', + areas: ['all'], + }, +] + +const MAINTAINERS = (() => { + const parsed = loadMaintainersFromNix() + return parsed.length > 0 ? parsed : FALLBACK_MAINTAINERS +})() + +// File patterns to components mapping +const FILE_PATTERNS = [ + { pattern: /^launcher\/ui\//, component: 'ui' }, + { pattern: /^launcher\/minecraft\//, component: 'minecraft' }, + { pattern: /^launcher\/modplatform\//, component: 'modplatform' }, + { pattern: /^launcher\//, component: 'core' }, + { pattern: /^libraries\//, component: 'core' }, + { pattern: /^\.github\//, component: 'ci' }, + { pattern: /^ci\//, component: 'ci' }, + { pattern: /CMakeLists\.txt$/, component: 'build' }, + { pattern: /\.cmake$/, component: 'build' }, + { pattern: /vcpkg/, component: 'build' }, + { pattern: /^docs\//, component: 'docs' }, + { pattern: /\.md$/, component: 'docs' }, + { pattern: /translations\//, component: 'translations' }, +] + +const COMPONENTS = Array.from(new Set(FILE_PATTERNS.map(({ component }) => component))) + +const getMaintainersForComponent = component => { + const assigned = MAINTAINERS.filter( + maintainer => + maintainer.areas.includes(component) || maintainer.areas.includes('all') + ).map(maintainer => maintainer.handle) + + return assigned.length > 0 ? assigned : MAINTAINERS.map(maintainer => maintainer.handle) +} + +// Component to reviewer mapping for ProjT Launcher +const COMPONENT_REVIEWERS = Object.fromEntries( + COMPONENTS.map(component => [component, getMaintainersForComponent(component)]) +) + +/** + * Get components affected by file changes + * @param {Array} files - List of changed files + * @returns {Set} Affected components + */ +function getAffectedComponents(files) { + const components = new Set() + + for (const file of files) { + const filename = file.filename || file + for (const { pattern, component } of FILE_PATTERNS) { + if (pattern.test(filename)) { + components.add(component) + break + } + } + } + + return components +} + +/** + * Get reviewers for components + * @param {Set} components - Affected components + * @returns {Set} Reviewers + */ +function getReviewersForComponents(components) { + const reviewers = new Set() + + for (const component of components) { + const componentReviewers = COMPONENT_REVIEWERS[component] || [] + for (const reviewer of componentReviewers) { + reviewers.add(reviewer.toLowerCase()) + } + } + + return reviewers +} + +/** + * Handle reviewer assignment for a PR + */ +async function handleReviewers({ + github, + context, + core, + log, + dry, + pull_request, + reviews, + maintainers, + owners, + getTeamMembers, + getUser, +}) { + const pull_number = pull_request.number + + // Get currently requested reviewers + const requested_reviewers = new Set( + pull_request.requested_reviewers.map(({ login }) => login.toLowerCase()), + ) + log?.( + 'reviewers - requested_reviewers', + Array.from(requested_reviewers).join(', '), + ) + + // Get existing reviewers (already reviewed) + const existing_reviewers = new Set( + reviews.map(({ user }) => user?.login.toLowerCase()).filter(Boolean), + ) + log?.( + 'reviewers - existing_reviewers', + Array.from(existing_reviewers).join(', '), + ) + + // Guard against too many reviewers from large PRs + if (maintainers && maintainers.length > 16) { + core.warning('Too many potential reviewers, skipping automatic assignment.') + return existing_reviewers.size === 0 && requested_reviewers.size === 0 + } + + // Build list of potential reviewers + const users = new Set() + + // Add maintainers + if (maintainers) { + for (const id of maintainers) { + try { + const user = await getUser(id) + users.add(user.login.toLowerCase()) + } catch (e) { + core.warning(`Could not resolve user ID ${id}`) + } + } + } + + // Add owners (from CODEOWNERS) + if (owners) { + for (const handle of owners) { + if (handle && !handle.includes('/')) { + users.add(handle.toLowerCase()) + } + } + } + + log?.('reviewers - users', Array.from(users).join(', ')) + + // Handle team-based owners + const teams = new Set() + if (owners) { + for (const handle of owners) { + const parts = handle.split('/') + if (parts.length === 2 && parts[0] === context.repo.owner) { + teams.add(parts[1]) + } + } + } + log?.('reviewers - teams', Array.from(teams).join(', ')) + + // Get team members + const team_members = new Set() + if (teams.size > 0 && getTeamMembers) { + for (const team of teams) { + try { + const members = await getTeamMembers(team) + for (const member of members) { + team_members.add(member.login.toLowerCase()) + } + } catch (e) { + core.warning(`Could not fetch team ${team}`) + } + } + } + log?.('reviewers - team_members', Array.from(team_members).join(', ')) + + // Combine all potential reviewers + const all_reviewers = new Set([...users, ...team_members]) + + // Remove PR author - can't review own PR + const author = pull_request.user?.login.toLowerCase() + all_reviewers.delete(author) + + log?.('reviewers - all_reviewers', Array.from(all_reviewers).join(', ')) + + // Filter to collaborators only + const reviewers = [] + for (const username of all_reviewers) { + try { + await github.rest.repos.checkCollaborator({ + ...context.repo, + username, + }) + reviewers.push(username) + } catch (e) { + if (e.status !== 404) throw e + core.warning( + `User ${username} cannot be requested for review (not a collaborator)`, + ) + } + } + log?.('reviewers - filtered_reviewers', reviewers.join(', ')) + + // Limit reviewers + if (reviewers.length > 10) { + core.warning(`Too many reviewers (${reviewers.length}), limiting to 10`) + reviewers.length = 10 + } + + // Determine who needs to be requested + const new_reviewers = new Set(reviewers) + .difference(requested_reviewers) + .difference(existing_reviewers) + + log?.( + 'reviewers - new_reviewers', + Array.from(new_reviewers).join(', '), + ) + + if (new_reviewers.size === 0) { + log?.('Has reviewer changes', 'false (no new reviewers)') + } else if (dry) { + core.info( + `Would request reviewers for #${pull_number}: ${Array.from(new_reviewers).join(', ')} (dry run)`, + ) + } else { + await github.rest.pulls.requestReviewers({ + ...context.repo, + pull_number, + reviewers: Array.from(new_reviewers), + }) + core.info( + `Requested reviewers for #${pull_number}: ${Array.from(new_reviewers).join(', ')}`, + ) + } + + // Return whether "needs-reviewers" label should be set + return ( + new_reviewers.size === 0 && + existing_reviewers.size === 0 && + requested_reviewers.size === 0 + ) +} + +module.exports = { + handleReviewers, + getAffectedComponents, + getReviewersForComponents, + COMPONENT_REVIEWERS, + FILE_PATTERNS, +} diff --git a/archived/projt-launcher/ci/github-script/reviews.js b/archived/projt-launcher/ci/github-script/reviews.js new file mode 100644 index 0000000000..749ebc7085 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/reviews.js @@ -0,0 +1,93 @@ +/** + * ProjT Launcher - Review Management + * Handles GitHub PR review operations + */ + +/** + * Dismiss all bot-created reviews on a PR + */ +async function dismissReviews({ github, context, dry }) { + const pull_number = context.payload.pull_request.number + + if (dry) { + return + } + + await Promise.all( + ( + await github.paginate(github.rest.pulls.listReviews, { + ...context.repo, + pull_number, + }) + ) + .filter((review) => review.user?.login === 'github-actions[bot]') + .map(async (review) => { + if (review.state === 'CHANGES_REQUESTED') { + await github.rest.pulls.dismissReview({ + ...context.repo, + pull_number, + review_id: review.id, + message: 'All good now, thank you!', + }) + } + await github.graphql( + `mutation($node_id:ID!) { + minimizeComment(input: { + classifier: RESOLVED, + subjectId: $node_id + }) + { clientMutationId } + }`, + { node_id: review.node_id }, + ) + }), + ) +} + +async function postReview({ github, context, core, dry, body }) { + const pull_number = context.payload.pull_request.number + + const pendingReview = ( + await github.paginate(github.rest.pulls.listReviews, { + ...context.repo, + pull_number, + }) + ).find( + (review) => + review.user?.login === 'github-actions[bot]' && + // If a review is still pending, we can just update this instead + // of posting a new one. + (review.state === 'CHANGES_REQUESTED' || + // No need to post a new review, if an older one with the exact + // same content had already been dismissed. + review.body === body), + ) + + if (dry) { + if (pendingReview) + core.info(`pending review found: ${pendingReview.html_url}`) + else core.info('no pending review found') + core.info(body) + } else { + if (pendingReview) { + await github.rest.pulls.updateReview({ + ...context.repo, + pull_number, + review_id: pendingReview.id, + body, + }) + } else { + await github.rest.pulls.createReview({ + ...context.repo, + pull_number, + event: 'REQUEST_CHANGES', + body, + }) + } + } +} + +module.exports = { + dismissReviews, + postReview, +} diff --git a/archived/projt-launcher/ci/github-script/run b/archived/projt-launcher/ci/github-script/run new file mode 100644 index 0000000000..4f296f5633 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/run @@ -0,0 +1,132 @@ +#!/usr/bin/env -S node --import ./run +/** + * ProjT Launcher - CI Script Runner + * CLI tool for running CI automation scripts locally + */ +import { execSync } from 'node:child_process' +import { closeSync, mkdtempSync, openSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { program } from 'commander' +import * as core from '@actions/core' +import { getOctokit } from '@actions/github' + +/** + * Run a CI action locally + */ +async function run(action, owner, repo, pull_number, options = {}) { + // Get GitHub token from gh CLI + const token = execSync('gh auth token', { encoding: 'utf-8' }).trim() + const github = getOctokit(token) + + // Build payload + const payload = !pull_number ? {} : { + pull_request: (await github.rest.pulls.get({ + owner, + repo, + pull_number, + })).data + } + + process.env['INPUT_GITHUB-TOKEN'] = token + + // Set up step summary file + closeSync(openSync('step-summary.md', 'w')) + process.env.GITHUB_STEP_SUMMARY = 'step-summary.md' + + await action({ + github, + context: { + payload, + repo: { + owner, + repo, + }, + }, + core, + dry: true, + ...options, + }) +} + +program + .name('projt-ci') + .description('ProjT Launcher CI automation script runner') + .version('1.0.0') + +program + .command('prepare') + .description('Prepare and validate a pull request') + .argument('', 'Repository owner (e.g., Project-Tick)') + .argument('', 'Repository name (e.g., ProjT-Launcher)') + .argument('', 'Pull Request number') + .option('--no-dry', 'Make actual modifications') + .action(async (owner, repo, pr, options) => { + const prepare = (await import('./prepare.js')).default + await run(prepare, owner, repo, pr, options) + }) + +program + .command('commits') + .description('Validate commit messages and structure') + .argument('', 'Repository owner') + .argument('', 'Repository name') + .argument('', 'Pull Request number') + .option('--no-dry', 'Make actual modifications') + .action(async (owner, repo, pr, options) => { + const commits = (await import('./commits.js')).default + await run(commits, owner, repo, pr, options) + }) + +program + .command('get-teams') + .description('Fetch team information from GitHub organization') + .argument('', 'Organization/owner name') + .argument('', 'Repository name') + .argument('[outFile]', 'Output file path (prints to stdout if omitted)') + .action(async (owner, repo, outFile, options) => { + const getTeams = (await import('./get-teams.js')).default + await run(getTeams, owner, repo, undefined, { ...options, outFile }) + }) + +program + .command('reviewers') + .description('Assign reviewers to a pull request') + .argument('', 'Repository owner') + .argument('', 'Repository name') + .argument('', 'Pull Request number') + .option('--no-dry', 'Make actual modifications') + .action(async (owner, repo, pr, options) => { + const { handleReviewers } = await import('./reviewers.js') + const token = execSync('gh auth token', { encoding: 'utf-8' }).trim() + const github = getOctokit(token) + + const pull_request = (await github.rest.pulls.get({ + owner, + repo, + pull_number: pr, + })).data + + const reviews = await github.paginate(github.rest.pulls.listReviews, { + owner, + repo, + pull_number: pr, + }) + + await handleReviewers({ + github, + context: { repo: { owner, repo } }, + core, + log: console.log, + dry: options.dry ?? true, + pull_request, + reviews, + maintainers: [], + owners: [], + getTeamMembers: async () => [], + getUser: async (id) => ({ login: `user-${id}`, id }), + }) + }) + +// Parse CLI arguments +await program.parse() diff --git a/archived/projt-launcher/ci/github-script/shell.nix b/archived/projt-launcher/ci/github-script/shell.nix new file mode 100644 index 0000000000..788fee1815 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/shell.nix @@ -0,0 +1,40 @@ +# ProjT Launcher - GitHub Script Development Shell +# Provides Node.js environment for CI automation scripts +{ + system ? builtins.currentSystem, + pkgs ? import { inherit system; }, +}: + +pkgs.mkShell { + name = "projt-launcher-github-script"; + + packages = with pkgs; [ + # Node.js for running scripts + nodejs_20 + + # GitHub CLI for authentication + gh + + # Optional: development tools + nodePackages.npm + ]; + + shellHook = '' + echo "ProjT Launcher GitHub Script Development Environment" + echo "" + echo "Available commands:" + echo " npm install - Install dependencies" + echo " ./run --help - Show available CLI commands" + echo " gh auth login - Authenticate with GitHub" + echo "" + + # Install npm dependencies if package-lock.json exists + if [ -f package-lock.json ] && [ ! -d node_modules ]; then + echo "Installing npm dependencies..." + npm ci + fi + ''; + + # Environment variables + PROJT_CI_ENV = "development"; +} diff --git a/archived/projt-launcher/ci/github-script/test/commits.test.js b/archived/projt-launcher/ci/github-script/test/commits.test.js new file mode 100644 index 0000000000..ed1be49682 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/test/commits.test.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +'use strict' + +process.env.COMMIT_TYPES = 'customtype' + +const assert = require('node:assert/strict') +const commits = require('../commits.js') + +const { validateCommitMessage, normalizeCommitType } = commits + +const validMessages = [ + 'feat(ui): add redesigned settings panel', + 'refactor: drop deprecated launcher flag support', + 'chore(ci): refresh workflows configuration', + '11.feat: support legacy numbered commit type format', + '23.deps(deps): bump dependency pins', + 'release: publish stable build artifacts', + 'customtype: allow env commit type overrides', +] + +for (const message of validMessages) { + const result = validateCommitMessage(message) + assert.equal( + result.valid, + true, + `Expected commit "${message}" to be valid, got: ${result.message}` + ) +} + +const invalidType = validateCommitMessage('unknown(scope): add feature that is real enough') +assert.equal(invalidType.valid, false, 'Expected invalid type to be rejected') +assert.match(invalidType.message, /Unknown commit type/i) + +const shortDescription = validateCommitMessage('feat: short') +assert.equal(shortDescription.valid, false, 'Expected short description to fail validation') +assert.match(shortDescription.message, /too short/i) + +assert.equal(normalizeCommitType('11.feat'), 'feat') +assert.equal(normalizeCommitType('23.deps'), 'deps') +assert.equal(normalizeCommitType('chore'), 'chore') + +console.log('commits.js tests passed') diff --git a/archived/projt-launcher/ci/github-script/withRateLimit.js b/archived/projt-launcher/ci/github-script/withRateLimit.js new file mode 100644 index 0000000000..e7fcfbb513 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/withRateLimit.js @@ -0,0 +1,86 @@ +/** + * ProjT Launcher - Rate Limit Handler + * Manages GitHub API rate limiting for CI scripts + */ + +module.exports = async ({ github, core, maxConcurrent = 1 }, callback) => { + let Bottleneck + try { + Bottleneck = require('bottleneck') + } catch (err) { + core?.warning?.('bottleneck not installed; running without explicit rate limiting') + Bottleneck = class { + constructor() {} + wrap(fn) { + return (...args) => fn(...args) + } + chain() { + return this + } + schedule(fn, ...args) { + return fn(...args) + } + updateSettings() {} + } + } + + const stats = { + issues: 0, + prs: 0, + requests: 0, + artifacts: 0, + } + + // Rate-Limiting and Throttling, see for details: + // https://github.com/octokit/octokit.js/issues/1069#throttling + // https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api + const allLimits = new Bottleneck({ + // Avoid concurrent requests + maxConcurrent, + // Will be updated with first `updateReservoir()` call below. + reservoir: 0, + }) + // Pause between mutative requests + const writeLimits = new Bottleneck({ minTime: 1000 }).chain(allLimits) + github.hook.wrap('request', async (request, options) => { + // Requests to a different host do not count against the rate limit. + if (options.url.startsWith('https://github.com')) return request(options) + // Requests to the /rate_limit endpoint do not count against the rate limit. + if (options.url === '/rate_limit') return request(options) + // Search requests are in a different resource group, which allows 30 requests / minute. + // We do less than a handful each run, so not implementing throttling for now. + if (options.url.startsWith('/search/')) return request(options) + stats.requests++ + if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) + return writeLimits.schedule(request.bind(null, options)) + else return allLimits.schedule(request.bind(null, options)) + }) + + async function updateReservoir() { + let response + try { + response = await github.rest.rateLimit.get() + } catch (err) { + core.error(`Failed updating reservoir:\n${err}`) + // Keep retrying on failed rate limit requests instead of exiting the script early. + return + } + // Always keep 1000 spare requests for other jobs to do their regular duty. + // They normally use below 100, so 1000 is *plenty* of room to work with. + const reservoir = Math.max(0, response.data.resources.core.remaining - 1000) + core.info(`Updating reservoir to: ${reservoir}`) + allLimits.updateSettings({ reservoir }) + } + await updateReservoir() + // Update remaining requests every minute to account for other jobs running in parallel. + const reservoirUpdater = setInterval(updateReservoir, 60 * 1000) + + try { + await callback(stats) + } finally { + clearInterval(reservoirUpdater) + core.notice( + `Processed ${stats.prs} PRs, ${stats.issues} Issues, made ${stats.requests + stats.artifacts} API requests and downloaded ${stats.artifacts} artifacts.`, + ) + } +} diff --git a/archived/projt-launcher/ci/nixpkgs-vet.nix b/archived/projt-launcher/ci/nixpkgs-vet.nix new file mode 100644 index 0000000000..610a13168b --- /dev/null +++ b/archived/projt-launcher/ci/nixpkgs-vet.nix @@ -0,0 +1,2 @@ +# Deprecated wrapper kept for backwards compatibility. +args: import ./code-quality.nix args diff --git a/archived/projt-launcher/ci/nixpkgs-vet.sh b/archived/projt-launcher/ci/nixpkgs-vet.sh new file mode 100644 index 0000000000..99382c36a8 --- /dev/null +++ b/archived/projt-launcher/ci/nixpkgs-vet.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Deprecated wrapper kept for backwards compatibility. +set -euo pipefail +echo "ci/nixpkgs-vet.sh is deprecated; use ci/code-quality.sh instead." >&2 +exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/code-quality.sh" "$@" + diff --git a/archived/projt-launcher/ci/parse.nix b/archived/projt-launcher/ci/parse.nix new file mode 100644 index 0000000000..96652c7a95 --- /dev/null +++ b/archived/projt-launcher/ci/parse.nix @@ -0,0 +1,38 @@ +# ProjT Launcher Nix File Parser +# Validates all .nix files in the project for syntax errors + +{ + lib, + nix, + runCommand, +}: +let + nixFiles = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.fileFilter (file: file.hasExt "nix") ../.; + }; +in +runCommand "projt-nix-parse-${nix.name}" + { + nativeBuildInputs = [ + nix + ]; + } + '' + export NIX_STORE_DIR=$TMPDIR/store + export NIX_STATE_DIR=$TMPDIR/state + nix-store --init + + cd "${nixFiles}" + + echo "Parsing Nix files in ProjT Launcher..." + + # Parse all .nix files to check for syntax errors + find . -type f -iname '*.nix' | while read file; do + echo "Checking: $file" + nix-instantiate --parse "$file" >/dev/null + done + + echo "All Nix files parsed successfully!" + touch $out + '' diff --git a/archived/projt-launcher/ci/pinned.json b/archived/projt-launcher/ci/pinned.json new file mode 100644 index 0000000000..abcc71877a --- /dev/null +++ b/archived/projt-launcher/ci/pinned.json @@ -0,0 +1,44 @@ +{ + "dependencies": { + "cmake": { + "version": "3.28.0", + "description": "Build system" + }, + "qt6": { + "version": "6.7.0", + "description": "Qt framework for UI" + }, + "gcc": { + "version": "13.2.0", + "description": "GCC compiler" + }, + "clang": { + "version": "17.0.0", + "description": "Clang compiler" + }, + "ninja": { + "version": "1.11.1", + "description": "Fast build system" + }, + "gtest": { + "version": "1.14.0", + "description": "Google Test framework" + } + }, + "platforms": { + "linux": { + "runner": "ubuntu-24.04", + "compiler": "gcc" + }, + "macos": { + "runner": "macos-14", + "compiler": "clang" + }, + "windows": { + "runner": "windows-2022", + "compiler": "msvc" + } + }, + "version": 1, + "updated": "2025-12-06" +} diff --git a/archived/projt-launcher/ci/supportedBranches.js b/archived/projt-launcher/ci/supportedBranches.js new file mode 100644 index 0000000000..dc9abb3ff8 --- /dev/null +++ b/archived/projt-launcher/ci/supportedBranches.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/** + * ProjT Launcher Branch Classifier + * Classifies branches for CI/CD purposes + */ + +// Branch type configuration for ProjT Launcher +const typeConfig = { + develop: ['development', 'primary'], + master: ['development', 'primary'], + main: ['development', 'primary'], + release: ['release', 'primary'], + feature: ['development', 'feature'], + bugfix: ['development', 'bugfix'], + hotfix: ['release', 'hotfix'], +} + +// Order ranks branches by priority for base branch selection +const orderConfig = { + develop: 0, + master: 1, + main: 1, + release: 2, + feature: 4, + bugfix: 4, + hotfix: 5, +} + +/** + * Split branch name into components + * @param {string} branch - Branch name + * @returns {object} Parsed branch components + */ +function split(branch) { + const match = branch.match( + /(?[a-z_-]+?)(-(?\d+\.\d+\.\d+|v?\d+\.\d+))?(-(?.*))?$/i + ) + return match ? match.groups : { prefix: branch } +} + +/** + * Classify a branch for CI purposes + * @param {string} branch - Branch name + * @returns {object} Branch classification + */ +function classify(branch) { + const { prefix, version, suffix } = split(branch) + const normalizedPrefix = prefix.toLowerCase().replace(/-/g, '_') + + return { + branch, + order: orderConfig[normalizedPrefix] ?? orderConfig[prefix.split('/')[0]] ?? Infinity, + isRelease: prefix.startsWith('release') || prefix.startsWith('hotfix'), + type: typeConfig[normalizedPrefix] ?? typeConfig[prefix.split('/')[0]] ?? ['wip'], + version: version ?? null, + suffix: suffix ?? null, + } +} + +/** + * Check if branch should trigger full CI + * @param {string} branch - Branch name + * @returns {boolean} + */ +function shouldRunFullCI(branch) { + const { type } = classify(branch) + return type.includes('primary') || type.includes('release') +} + +/** + * Get target branch for backport + * @param {string} branch - Branch name + * @returns {string|null} + */ +function getBackportTarget(branch) { + const { isRelease, version } = classify(branch) + if (isRelease && version) { + return `release-${version}` + } + return null +} + +module.exports = { classify, split, shouldRunFullCI, getBackportTarget } + +// CLI tests when run directly +if (require.main === module) { + console.log('ProjT Launcher Branch Classifier Tests\n') + + console.log('split(branch):') + const testSplits = ['develop', 'release-1.0.0', 'feature/new-ui', 'hotfix-1.0.1'] + testSplits.forEach(b => console.log(` ${b}:`, split(b))) + + console.log('\nclassify(branch):') + const testClassify = ['develop', 'master', 'release-1.0.0', 'feature/settings', 'bugfix/crash-fix'] + testClassify.forEach(b => console.log(` ${b}:`, classify(b))) + + console.log('\nshouldRunFullCI(branch):') + testClassify.forEach(b => console.log(` ${b}: ${shouldRunFullCI(b)}`)) +} diff --git a/archived/projt-launcher/ci/supportedSystems.json b/archived/projt-launcher/ci/supportedSystems.json new file mode 100644 index 0000000000..9441c8a9e4 --- /dev/null +++ b/archived/projt-launcher/ci/supportedSystems.json @@ -0,0 +1,64 @@ +{ + "build": [ + { + "name": "linux-x64", + "os": "ubuntu-24.04", + "arch": "x86_64", + "nix": "x86_64-linux" + }, + { + "name": "linux-arm64", + "os": "ubuntu-24.04-arm", + "arch": "aarch64", + "nix": "aarch64-linux" + }, + { + "name": "macos-arm64", + "os": "macos-14", + "arch": "arm64", + "nix": "aarch64-darwin" + }, + { + "name": "windows-x64", + "os": "windows-2022", + "arch": "x86_64", + "compiler": "msvc" + }, + { + "name": "windows-arm64", + "os": "windows-11-arm", + "arch": "aarch64", + "compiler": "msvc" + }, + { + "name": "windows-mingw-x64", + "os": "windows-2022", + "arch": "x86_64", + "compiler": "mingw", + "msystem": "CLANG64" + }, + { + "name": "windows-mingw-arm64", + "os": "windows-11-arm", + "arch": "aarch64", + "compiler": "mingw", + "msystem": "CLANGARM64" + } + ], + "test": [ + "linux-x64", + "linux-arm64", + "macos-arm64", + "windows-x64", + "windows-arm64" + ], + "package": [ + "linux-x64", + "linux-arm64", + "macos-arm64", + "windows-x64", + "windows-arm64", + "windows-mingw-x64", + "windows-mingw-arm64" + ] +} diff --git a/archived/projt-launcher/ci/supportedVersions.nix b/archived/projt-launcher/ci/supportedVersions.nix new file mode 100644 index 0000000000..60d2ca9fe3 --- /dev/null +++ b/archived/projt-launcher/ci/supportedVersions.nix @@ -0,0 +1,68 @@ +# ProjT Launcher - Supported Dependency Versions +# Returns a list of supported Qt and compiler versions for testing + +{ + pkgs ? import { }, +}: +let + inherit (pkgs) lib; + + # Supported Qt versions + qtVersions = [ + "qt6Packages" # Qt 6.x (primary) + ]; + + # Supported compiler versions + compilerVersions = { + gcc = [ + "gcc13" + "gcc14" + ]; + clang = [ + "clang_17" + "clang_18" + ]; + }; + + # Supported CMake versions + cmakeVersions = [ + "cmake" # Latest stable + ]; + + # Build matrix combinations + buildMatrix = lib.flatten ( + map ( + qt: + map (compiler: { + inherit qt; + inherit compiler; + cmake = "cmake"; + }) (compilerVersions.gcc ++ compilerVersions.clang) + ) qtVersions + ); + +in +{ + inherit + qtVersions + compilerVersions + cmakeVersions + buildMatrix + ; + + # Minimum required versions + minimum = { + cmake = "3.22"; + qt = "6.5"; + gcc = "12"; + clang = "15"; + }; + + # Recommended versions + recommended = { + cmake = "3.28"; + qt = "6.7"; + gcc = "14"; + clang = "18"; + }; +} diff --git a/archived/projt-launcher/ci/update-pinned.sh b/archived/projt-launcher/ci/update-pinned.sh new file mode 100644 index 0000000000..105238e2a4 --- /dev/null +++ b/archived/projt-launcher/ci/update-pinned.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# ProjT Launcher - Update pinned dependency versions +# Updates ci/pinned.json with current recommended versions + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PINNED_FILE="$SCRIPT_DIR/pinned.json" + +echo "Updating ProjT Launcher pinned dependencies..." + +# Get current date +CURRENT_DATE=$(date +%Y-%m-%d) + +# Create updated pinned.json +cat > "$PINNED_FILE" << EOF +{ + "dependencies": { + "cmake": { + "version": "3.28.0", + "description": "Build system" + }, + "qt6": { + "version": "6.7.0", + "description": "Qt framework for UI" + }, + "gcc": { + "version": "13.2.0", + "description": "GCC compiler" + }, + "clang": { + "version": "17.0.0", + "description": "Clang compiler" + }, + "ninja": { + "version": "1.11.1", + "description": "Fast build system" + }, + "gtest": { + "version": "1.14.0", + "description": "Google Test framework" + } + }, + "platforms": { + "linux": { + "runner": "ubuntu-24.04", + "compiler": "gcc" + }, + "macos": { + "runner": "macos-14", + "compiler": "clang" + }, + "windows": { + "runner": "windows-2022", + "compiler": "msvc" + } + }, + "version": 1, + "updated": "$CURRENT_DATE" +} +EOF + +echo "Updated $PINNED_FILE" +echo "Date: $CURRENT_DATE" diff --git a/archived/projt-launcher/cmake/CompilerWarnings.cmake b/archived/projt-launcher/cmake/CompilerWarnings.cmake new file mode 100644 index 0000000000..e6b1a7415a --- /dev/null +++ b/archived/projt-launcher/cmake/CompilerWarnings.cmake @@ -0,0 +1,164 @@ +# +# Function to set compiler warnings with reasonable defaults at the project level. +# Taken from https://github.com/aminya/project_options/blob/main/src/CompilerWarnings.cmake +# under the folowing license: +# +# MIT License +# +# Copyright (c) 2022-2100 Amin Yahyaabadi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +include_guard() + +function(_set_project_warnings_add_target_link_option TARGET OPTIONS) + target_link_options(${_project_name} INTERFACE ${OPTIONS}) +endfunction() + +# Set the compiler warnings +# +# https://clang.llvm.org/docs/DiagnosticsReference.html +# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md +function( + set_project_warnings + _project_name + MSVC_WARNINGS + CLANG_WARNINGS + GCC_WARNINGS +) + if("${MSVC_WARNINGS}" STREQUAL "") + set(MSVC_WARNINGS + /W4 # Baseline reasonable warnings + /w14242 # 'identifier': conversion from 'type1' to 'type1', possible loss of data + /w14254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data + /w14263 # 'function': member function does not override any base class virtual member function + /w14265 # 'classname': class has virtual functions, but destructor is not virtual instances of this class may not + # be destructed correctly + /w14287 # 'operator': unsigned/negative constant mismatch + /we4289 # nonstandard extension used: 'variable': loop control variable declared in the for-loop is used outside + # the for-loop scope + /w14296 # 'operator': expression is always 'boolean_value' + /w14311 # 'variable': pointer truncation from 'type1' to 'type2' + /w14545 # expression before comma evaluates to a function which is missing an argument list + /w14546 # function call before comma missing argument list + /w14547 # 'operator': operator before comma has no effect; expected operator with side-effect + /w14549 # 'operator': operator before comma has no effect; did you intend 'operator'? + /w14555 # expression has no effect; expected expression with side- effect + /w14619 # pragma warning: there is no warning number 'number' + /w14640 # Enable warning on thread un-safe static member initialization + /w14826 # Conversion from 'type1' to 'type_2' is sign-extended. This may cause unexpected runtime behavior. + /w14905 # wide string literal cast to 'LPSTR' + /w14906 # string literal cast to 'LPWSTR' + /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied + /permissive- # standards conformance mode for MSVC compiler. + + /we4062 # forbid omitting a possible value of an enum in a switch statement + ) + endif() + + if("${CLANG_WARNINGS}" STREQUAL "") + set(CLANG_WARNINGS + -Wall + -Wextra # reasonable and standard + -Wshadow # warn the user if a variable declaration shadows one from a parent context + -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps + # catch hard to track down memory errors + -Wold-style-cast # warn for c-style casts + -Wcast-align # warn for potential performance problem casts + -Wunused # warn on anything being unused + -Woverloaded-virtual # warn if you overload (not override) a virtual function + -Wpedantic # warn if non-standard C++ is used + -Wconversion # warn on type conversions that may lose data + -Wsign-conversion # warn on sign conversions + -Wnull-dereference # warn if a null dereference is detected + -Wdouble-promotion # warn if float is implicit promoted to double + -Wformat=2 # warn on security issues around functions that format output (ie printf) + -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation + # -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results + # in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour + # instead of the exact standard wording so we can safely ignore it + -Wno-gnu-zero-variadic-macro-arguments + + -Werror=switch # forbid omitting a possible value of an enum in a switch statement + ) + endif() + + if("${GCC_WARNINGS}" STREQUAL "") + set(GCC_WARNINGS + ${CLANG_WARNINGS} + -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist + -Wduplicated-cond # warn if if / else chain has duplicated conditions + -Wduplicated-branches # warn if if / else branches have duplicated code + -Wlogical-op # warn about logical operations being used where bitwise were probably wanted + -Wuseless-cast # warn if you perform a cast to the same type + + -Werror=switch # forbid omitting a possible value of an enum in a switch statement + ) + endif() + + if(MSVC) + set(PROJECT_WARNINGS_CXX ${MSVC_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + set(PROJECT_WARNINGS_CXX ${CLANG_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(PROJECT_WARNINGS_CXX ${GCC_WARNINGS}) + list(REMOVE_ITEM PROJECT_WARNINGS_CXX -Wno-gnu-zero-variadic-macro-arguments) + else() + message(AUTHOR_WARNING "No compiler warnings set for CXX compiler: '${CMAKE_CXX_COMPILER_ID}'") + # TODO support Intel compiler + endif() + + # Add C warnings + set(PROJECT_WARNINGS_C "${PROJECT_WARNINGS_CXX}") + list( + REMOVE_ITEM + PROJECT_WARNINGS_C + -Wnon-virtual-dtor + -Wold-style-cast + -Woverloaded-virtual + -Wuseless-cast + -Wextra-semi + + -Werror=switch # forbid omitting a possible value of an enum in a switch statement + ) + + target_compile_options( + ${_project_name} + INTERFACE # C++ warnings + $<$:${PROJECT_WARNINGS_CXX}> + # C warnings + $<$:${PROJECT_WARNINGS_C}> + ) + + # If we are using the compiler as a linker driver pass the warnings to it + # (most useful when using LTO or warnings as errors) + if(CMAKE_CXX_LINK_EXECUTABLE MATCHES "^") + _set_project_warnings_add_target_link_option( + ${_project_name} "$<$:${PROJECT_WARNINGS_CXX}>" + ) + endif() + + if(CMAKE_C_LINK_EXECUTABLE MATCHES "^") + _set_project_warnings_add_target_link_option( + ${_project_name} "$<$:${PROJECT_WARNINGS_C}>" + ) + endif() + + endfunction() diff --git a/archived/projt-launcher/cmake/ECMQueryQt.cmake b/archived/projt-launcher/cmake/ECMQueryQt.cmake new file mode 100644 index 0000000000..98eb50089e --- /dev/null +++ b/archived/projt-launcher/cmake/ECMQueryQt.cmake @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2014 Rohan Garg +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2014-2016 Aleix Pol +# SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau +# SPDX-FileCopyrightText: 2022 Ahmad Samir +# +# SPDX-License-Identifier: BSD-3-Clause +#[=======================================================================[.rst: +ECMQueryQt +--------------- +This module can be used to query the installation paths used by Qt. + +For Qt5 this uses ``qmake``, and for Qt6 this used ``qtpaths`` (the latter has built-in +support to query the paths of a target platform when cross-compiling). + +This module defines the following function: +:: + + ecm_query_qt( [TRY]) + +Passing ``TRY`` will result in the method not making the build fail if the executable +used for querying has not been found, but instead simply print a warning message and +return an empty string. + +Example usage: + +.. code-block:: cmake + + include(ECMQueryQt) + ecm_query_qt(bin_dir QT_INSTALL_BINS) + +If the call succeeds ``${bin_dir}`` will be set to ``/path/to/bin/dir`` (e.g. +``/usr/lib64/qt/bin/``). + +Since: 5.93 +#]=======================================================================] + +include(${CMAKE_CURRENT_LIST_DIR}/QtVersionOption.cmake) +include(CheckLanguage) +check_language(CXX) +if (CMAKE_CXX_COMPILER) + # Enable the CXX language to let CMake look for config files in library dirs. + # See: https://gitlab.kitware.com/cmake/cmake/-/issues/23266 + enable_language(CXX) +endif() + +if (QT_MAJOR_VERSION STREQUAL "5") + # QUIET to accommodate the TRY option + find_package(Qt${QT_MAJOR_VERSION}Core QUIET) + if(TARGET Qt5::qmake) + get_target_property(_qmake_executable_default Qt5::qmake LOCATION) + + set(QUERY_EXECUTABLE ${_qmake_executable_default} + CACHE FILEPATH "Location of the Qt5 qmake executable") + set(_exec_name_text "Qt5 qmake") + set(_cli_option "-query") + endif() +elseif(QT_MAJOR_VERSION STREQUAL "6") + # QUIET to accommodate the TRY option + find_package(Qt6 COMPONENTS CoreTools QUIET CONFIG) + if (TARGET Qt6::qtpaths) + get_target_property(_qtpaths_executable Qt6::qtpaths LOCATION) + + set(QUERY_EXECUTABLE ${_qtpaths_executable} + CACHE FILEPATH "Location of the Qt6 qtpaths executable") + set(_exec_name_text "Qt6 qtpaths") + set(_cli_option "--query") + endif() +endif() + +function(ecm_query_qt result_variable qt_variable) + set(options TRY) + set(oneValueArgs) + set(multiValueArgs) + + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT QUERY_EXECUTABLE) + if(ARGS_TRY) + set(${result_variable} "" PARENT_SCOPE) + message(STATUS "No ${_exec_name_text} executable found. Can't check ${qt_variable}") + return() + else() + message(FATAL_ERROR "No ${_exec_name_text} executable found. Can't check ${qt_variable} as required") + endif() + endif() + execute_process( + COMMAND ${QUERY_EXECUTABLE} ${_cli_option} "${qt_variable}" + RESULT_VARIABLE return_code + OUTPUT_VARIABLE output + ) + if(return_code EQUAL 0) + string(STRIP "${output}" output) + file(TO_CMAKE_PATH "${output}" output_path) + set(${result_variable} "${output_path}" PARENT_SCOPE) + else() + message(WARNING "Failed call: ${_command} \"${qt_variable}\"") + message(FATAL_ERROR "${_exec_name_text} call failed: ${return_code}") + endif() +endfunction() diff --git a/archived/projt-launcher/cmake/GetGitRevisionDescription.cmake b/archived/projt-launcher/cmake/GetGitRevisionDescription.cmake new file mode 100644 index 0000000000..4fbd90db79 --- /dev/null +++ b/archived/projt-launcher/cmake/GetGitRevisionDescription.cmake @@ -0,0 +1,284 @@ +# - Returns a version string from Git +# +# These functions force a re-configure on each git commit so that you can +# trust the values of the variables in your build system. +# +# get_git_head_revision( [ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR]) +# +# Returns the refspec and sha hash of the current head revision +# +# git_describe( [ ...]) +# +# Returns the results of git describe on the source tree, and adjusting +# the output so that it tests false if an error occurs. +# +# git_describe_working_tree( [ ...]) +# +# Returns the results of git describe on the working tree (--dirty option), +# and adjusting the output so that it tests false if an error occurs. +# +# git_get_exact_tag( [ ...]) +# +# Returns the results of git describe --exact-match on the source tree, +# and adjusting the output so that it tests false if there was no exact +# matching tag. +# +# git_local_changes() +# +# Returns either "CLEAN" or "DIRTY" with respect to uncommitted changes. +# Uses the return code of "git diff-index --quiet HEAD --". +# Does not regard untracked files. +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2020 Ryan Pavlik +# http://academic.cleardefinition.com +# +# Copyright 2009-2013, Iowa State University. +# Copyright 2013-2020, Ryan Pavlik +# Copyright 2013-2020, Contributors +# SPDX-License-Identifier: BSL-1.0 +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +if(__get_git_revision_description) + return() +endif() +set(__get_git_revision_description YES) + +# We must run the following at "include" time, not at function call time, +# to find the path to this module rather than the path to a calling list file +get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) + +# Function _git_find_closest_git_dir finds the next closest .git directory +# that is part of any directory in the path defined by _start_dir. +# The result is returned in the parent scope variable whose name is passed +# as variable _git_dir_var. If no .git directory can be found, the +# function returns an empty string via _git_dir_var. +# +# Example: Given a path C:/bla/foo/bar and assuming C:/bla/.git exists and +# neither foo nor bar contain a file/directory .git. This wil return +# C:/bla/.git +# +function(_git_find_closest_git_dir _start_dir _git_dir_var) + set(cur_dir "${_start_dir}") + set(git_dir "${_start_dir}/.git") + while(NOT EXISTS "${git_dir}") + # .git dir not found, search parent directories + set(git_previous_parent "${cur_dir}") + get_filename_component(cur_dir "${cur_dir}" DIRECTORY) + if(cur_dir STREQUAL git_previous_parent) + # We have reached the root directory, we are not in git + set(${_git_dir_var} + "" + PARENT_SCOPE) + return() + endif() + set(git_dir "${cur_dir}/.git") + endwhile() + set(${_git_dir_var} + "${git_dir}" + PARENT_SCOPE) +endfunction() + +function(get_git_head_revision _refspecvar _hashvar) + _git_find_closest_git_dir("${CMAKE_CURRENT_SOURCE_DIR}" GIT_DIR) + + if("${ARGN}" STREQUAL "ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR") + set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR TRUE) + else() + set(ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR FALSE) + endif() + if(NOT "${GIT_DIR}" STREQUAL "") + file(RELATIVE_PATH _relative_to_source_dir "${CMAKE_SOURCE_DIR}" + "${GIT_DIR}") + if("${_relative_to_source_dir}" MATCHES "[.][.]" AND NOT ALLOW_LOOKING_ABOVE_CMAKE_SOURCE_DIR) + # We've gone above the CMake root dir. + set(GIT_DIR "") + endif() + endif() + if("${GIT_DIR}" STREQUAL "") + set(${_refspecvar} + "GITDIR-NOTFOUND" + PARENT_SCOPE) + set(${_hashvar} + "GITDIR-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + # Check if the current source dir is a git submodule or a worktree. + # In both cases .git is a file instead of a directory. + # + if(NOT IS_DIRECTORY ${GIT_DIR}) + # The following git command will return a non empty string that + # points to the super project working tree if the current + # source dir is inside a git submodule. + # Otherwise the command will return an empty string. + # + execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse + --show-superproject-working-tree + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT "${out}" STREQUAL "") + # If out is empty, GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a submodule + file(READ ${GIT_DIR} submodule) + string(REGEX REPLACE "gitdir: (.*)$" "\\1" GIT_DIR_RELATIVE + ${submodule}) + string(STRIP ${GIT_DIR_RELATIVE} GIT_DIR_RELATIVE) + get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) + get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} + ABSOLUTE) + set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") + else() + # GIT_DIR/CMAKE_CURRENT_SOURCE_DIR is in a worktree + file(READ ${GIT_DIR} worktree_ref) + # The .git directory contains a path to the worktree information directory + # inside the parent git repo of the worktree. + # + string(REGEX REPLACE "gitdir: (.*)$" "\\1" git_worktree_dir + ${worktree_ref}) + string(STRIP ${git_worktree_dir} git_worktree_dir) + _git_find_closest_git_dir("${git_worktree_dir}" GIT_DIR) + set(HEAD_SOURCE_FILE "${git_worktree_dir}/HEAD") + endif() + else() + set(HEAD_SOURCE_FILE "${GIT_DIR}/HEAD") + endif() + set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") + if(NOT EXISTS "${GIT_DATA}") + file(MAKE_DIRECTORY "${GIT_DATA}") + endif() + + if(NOT EXISTS "${HEAD_SOURCE_FILE}") + return() + endif() + set(HEAD_FILE "${GIT_DATA}/HEAD") + configure_file("${HEAD_SOURCE_FILE}" "${HEAD_FILE}" COPYONLY) + + configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" + "${GIT_DATA}/grabRef.cmake" @ONLY) + include("${GIT_DATA}/grabRef.cmake") + + set(${_refspecvar} + "${HEAD_REF}" + PARENT_SCOPE) + set(${_hashvar} + "${HEAD_HASH}" + PARENT_SCOPE) +endfunction() + +function(git_describe _var) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + get_git_head_revision(refspec hash) + if(NOT GIT_FOUND) + set(${_var} + "GIT-NOTFOUND" + PARENT_SCOPE) + return() + endif() + if(NOT hash) + set(${_var} + "HEAD-HASH-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + # TODO sanitize + #if((${ARGN}" MATCHES "&&") OR + # (ARGN MATCHES "||") OR + # (ARGN MATCHES "\\;")) + # message("Please report the following error to the project!") + # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") + #endif() + + #message(STATUS "Arguments to execute_process: ${ARGN}") + + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --tags --always ${hash} ${ARGN} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE res + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT res EQUAL 0) + set(out "${out}-${res}-NOTFOUND") + endif() + + set(${_var} + "${out}" + PARENT_SCOPE) +endfunction() + +function(git_describe_working_tree _var) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + if(NOT GIT_FOUND) + set(${_var} + "GIT-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --dirty ${ARGN} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE res + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT res EQUAL 0) + set(out "${out}-${res}-NOTFOUND") + endif() + + set(${_var} + "${out}" + PARENT_SCOPE) +endfunction() + +function(git_get_exact_tag _var) + git_describe(out --exact-match ${ARGN}) + set(${_var} + "${out}" + PARENT_SCOPE) +endfunction() + +function(git_local_changes _var) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + get_git_head_revision(refspec hash) + if(NOT GIT_FOUND) + set(${_var} + "GIT-NOTFOUND" + PARENT_SCOPE) + return() + endif() + if(NOT hash) + set(${_var} + "HEAD-HASH-NOTFOUND" + PARENT_SCOPE) + return() + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD -- + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE res + OUTPUT_VARIABLE out + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(res EQUAL 0) + set(${_var} + "CLEAN" + PARENT_SCOPE) + else() + set(${_var} + "DIRTY" + PARENT_SCOPE) + endif() +endfunction() diff --git a/archived/projt-launcher/cmake/GetGitRevisionDescription.cmake.in b/archived/projt-launcher/cmake/GetGitRevisionDescription.cmake.in new file mode 100644 index 0000000000..116efc4e4e --- /dev/null +++ b/archived/projt-launcher/cmake/GetGitRevisionDescription.cmake.in @@ -0,0 +1,43 @@ +# +# Internal file for GetGitRevisionDescription.cmake +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2010 Ryan Pavlik +# http://academic.cleardefinition.com +# Iowa State University HCI Graduate Program/VRAC +# +# Copyright 2009-2012, Iowa State University +# Copyright 2011-2015, Contributors +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# SPDX-License-Identifier: BSL-1.0 + +set(HEAD_HASH) + +file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) + +string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) +if(HEAD_CONTENTS MATCHES "ref") + # named branch + string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") + if(EXISTS "@GIT_DIR@/${HEAD_REF}") + configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) + else() + configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY) + file(READ "@GIT_DATA@/packed-refs" PACKED_REFS) + if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") + set(HEAD_HASH "${CMAKE_MATCH_1}") + endif() + endif() +else() + # detached HEAD + configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) +endif() + +if(NOT HEAD_HASH) + file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) + string(STRIP "${HEAD_HASH}" HEAD_HASH) +endif() diff --git a/archived/projt-launcher/cmake/GitFunctions.cmake b/archived/projt-launcher/cmake/GitFunctions.cmake new file mode 100644 index 0000000000..a055b5de65 --- /dev/null +++ b/archived/projt-launcher/cmake/GitFunctions.cmake @@ -0,0 +1,37 @@ +if(__GITFUNCTIONS_CMAKE__) + return() +endif() +set(__GITFUNCTIONS_CMAKE__ TRUE) + +find_package(Git QUIET) + +include(CMakeParseArguments) + +if(GIT_FOUND) + function(git_run) + set(oneValueArgs OUTPUT_VAR DEFAULT) + set(multiValueArgs COMMAND) + cmake_parse_arguments(GIT_RUN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_RUN_COMMAND} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE GIT_RESULTVAR + OUTPUT_VARIABLE GIT_OUTVAR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if(GIT_RESULTVAR EQUAL 0) + set(${GIT_RUN_OUTPUT_VAR} "${GIT_OUTVAR}" PARENT_SCOPE) + else() + set(${GIT_RUN_OUTPUT_VAR} ${GIT_RUN_DEFAULT}) + message(STATUS "Failed to run Git: ${GIT_OUTVAR}") + endif() + endfunction() +else() + function(git_run) + set(oneValueArgs OUTPUT_VAR DEFAULT) + set(multiValueArgs COMMAND) + cmake_parse_arguments(GIT_RUN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(${GIT_RUN_OUTPUT_VAR} ${GIT_RUN_DEFAULT}) + endfunction(git_run) +endif() diff --git a/archived/projt-launcher/cmake/LauncherPackaging.cmake b/archived/projt-launcher/cmake/LauncherPackaging.cmake new file mode 100644 index 0000000000..569a316c4e --- /dev/null +++ b/archived/projt-launcher/cmake/LauncherPackaging.cmake @@ -0,0 +1,72 @@ +set(Launcher_PackageName "projtlauncher" CACHE STRING "Package name for distro packages") +set(Launcher_PackageVendor "Project Tick" CACHE STRING "Package vendor for distro packages") +set(Launcher_PackageContact "Project Tick" CACHE STRING "Package contact/maintainer") +set(Launcher_PackageDescription "${Launcher_DisplayName} - Minecraft Launcher" CACHE STRING "Package description summary") + +set(Launcher_ARCH_MAINTAINER "Mehmet Samet Duman " CACHE STRING "PKGBUILD maintainer") +set(Launcher_ARCH_PKGREL "2" CACHE STRING "PKGBUILD pkgrel") +set(Launcher_ARCH_PKGDESC "A Minecraft launcher and automation stack for long-term project health. " CACHE STRING "PKGBUILD description") +set(Launcher_ARCH_URL "https://projecttick.org" CACHE STRING "PKGBUILD URL") +set(Launcher_ARCH_ARCHS "x86_64 aarch64" CACHE STRING "PKGBUILD arch list") +set(Launcher_ARCH_LICENSES "GPL-3.0-only AND LGPL-3.0-or-later AND LGPL-2.0-or-later AND Apache-2.0 AND MIT AND LicenseRef-Batch AND OFL-1.1 AND Zlib AND bzip2" + CACHE STRING "PKGBUILD license string") +set(Launcher_ARCH_DEPENDS "gcc-libs glibc hicolor-icon-theme java-runtime=17 java-runtime=21 libgl qt6-5compat qt6-base qt6-imageformats qt6-networkauth qt6-svg" + CACHE STRING "Space-separated Arch Linux dependencies for PKGBUILD") +set(Launcher_ARCH_MAKEDEPENDS "cmake ghc-filesystem git jdk21-openjdk scdoc" + CACHE STRING "Space-separated Arch Linux makedepends for PKGBUILD") +set(Launcher_ARCH_OPTDEPENDS "'glfw: to use system GLFW libraries' 'openal: to use system OpenAL libraries' 'visualvm: Profiling support' 'xorg-xrandr: for older minecraft versions' 'java-runtime=8: for older minecraft versions' 'flite: minecraft voice narration'" + CACHE STRING "Quoted Arch Linux optdepends entries for PKGBUILD") +set(Launcher_ARCH_SHA256SUMS "'SKIP' '2ee3ba8d96e9882150783b6444651ea4a65d779532ecac8646f2ecd3a48c2770' '009e25d32aab6dbae193aac4b82fa1a26cb07f288225b2906da425a0f219bc4c' '32646946afc31ef5a4ce2cbb5a5a68a9f552c540a78ef23344c51c3efca58fa6'" + CACHE STRING "Quoted sha256sums list for PKGBUILD") + +set(CPACK_PACKAGE_NAME "${Launcher_PackageName}") +set(CPACK_PACKAGE_VENDOR "${Launcher_PackageVendor}") +set(CPACK_PACKAGE_CONTACT "${Launcher_PackageContact}") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${Launcher_PackageDescription}") + +set(CPACK_PACKAGE_VERSION "${Launcher_VERSION_NAME}") +set(CPACK_PACKAGE_VERSION_MAJOR "${Launcher_VERSION_MAJOR}") +set(CPACK_PACKAGE_VERSION_MINOR "${Launcher_VERSION_MINOR}") +set(CPACK_PACKAGE_VERSION_PATCH "${Launcher_VERSION_PATCH}") + +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/COPYING.md") +set(CPACK_PACKAGING_INSTALL_PREFIX "/usr") +# Only override the install prefix during packaging (not regular installs). +# Using CPACK_SET_DESTDIR=OFF makes CPack apply CPACK_PACKAGING_INSTALL_PREFIX +# instead of CMAKE_INSTALL_PREFIX. +set(CPACK_SET_DESTDIR OFF) +set(CPACK_COMPONENTS_ALL Runtime Development) + +# Debian packaging +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${Launcher_PackageContact}") +set(CPACK_DEBIAN_PACKAGE_SECTION "games") +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) +if(DEFINED Launcher_DEB_DEPENDS AND NOT Launcher_DEB_DEPENDS STREQUAL "") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${Launcher_DEB_DEPENDS}") +endif() + +# Fedora / RPM packaging +set(CPACK_RPM_PACKAGE_LICENSE "GPL-3.0-only") +set(CPACK_RPM_PACKAGE_GROUP "Games") +set(CPACK_RPM_PACKAGE_AUTOREQ ON) +set(CPACK_RPM_COMPONENT_INSTALL ON) +set(CPACK_RPM_MAIN_COMPONENT Runtime) +set(CPACK_RPM_DEVELOPMENT_PACKAGE_NAME "${CPACK_PACKAGE_NAME}-devel") +set(CPACK_RPM_DEVELOPMENT_PACKAGE_SUMMARY "${CPACK_PACKAGE_NAME} development files") +set(CPACK_RPM_DEVELOPMENT_PACKAGE_REQUIRES "${CPACK_PACKAGE_NAME} = ${CPACK_PACKAGE_VERSION}") +if(DEFINED Launcher_RPM_REQUIRES AND NOT Launcher_RPM_REQUIRES STREQUAL "") + set(CPACK_RPM_PACKAGE_REQUIRES "${Launcher_RPM_REQUIRES}") +endif() + +# Source RPMs are generated via CPackSourceConfig.cmake (cpack -G RPM --config CPackSourceConfig.cmake) +set(CPACK_SOURCE_RPM ON) + +# Generate PKGBUILD template for Arch +set(_arch_pkgbuild_in "${CMAKE_CURRENT_SOURCE_DIR}/packaging/arch/PKGBUILD.in") +if(EXISTS "${_arch_pkgbuild_in}") + set(_arch_pkgbuild_out "${CMAKE_CURRENT_BINARY_DIR}/packaging/arch/PKGBUILD") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/packaging/arch") + configure_file("${_arch_pkgbuild_in}" "${_arch_pkgbuild_out}" @ONLY) +endif() + +include(CPack) diff --git a/archived/projt-launcher/cmake/MacOSXBundleInfo.plist.in b/archived/projt-launcher/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 0000000000..fe90af76cd --- /dev/null +++ b/archived/projt-launcher/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,94 @@ + + + + + NSCameraUsageDescription + A Minecraft mod wants to access your camera. + NSMicrophoneUsageDescription + A Minecraft mod wants to access your microphone. + NSDownloadsFolderUsageDescription + ${Launcher_DisplayName} uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where ${Launcher_DisplayName} scans for downloaded mods in Settings or the prompt that appears. + NSLocalNetworkUsageDescription + Minecraft uses the local network to find and connect to LAN servers. + NSPrincipalClass + NSApplication + NSHighResolutionCapable + True + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + SUPublicEDKey + ${MACOSX_SPARKLE_UPDATE_PUBLIC_KEY} + SUFeedURL + ${MACOSX_SPARKLE_UPDATE_FEED_URL} + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + zip + mrpack + + CFBundleTypeName + ${Launcher_DisplayName} instance + CFBundleTypeOSTypes + + TEXT + utxt + TUTX + **** + + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleURLTypes + + + CFBundleURLName + Curseforge + CFBundleURLSchemes + + curseforge + + + + CFBundleURLName + ${Launcher_Name} + CFBundleURLSchemes + + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + + + + + diff --git a/archived/projt-launcher/cmake/QtVersionOption.cmake b/archived/projt-launcher/cmake/QtVersionOption.cmake new file mode 100644 index 0000000000..1390f9db60 --- /dev/null +++ b/archived/projt-launcher/cmake/QtVersionOption.cmake @@ -0,0 +1,38 @@ +#.rst: +# QtVersionOption +# --------------- +# +# Adds a build option to select the major Qt version if necessary, +# that is, if the major Qt version has not yet been determined otherwise +# (e.g. by a corresponding find_package() call). +# +# This module is typically included by other modules requiring knowledge +# about the major Qt version. +# +# ``QT_MAJOR_VERSION`` is defined to either be "5" or "6". +# +# +# Since 5.82.0. + +#============================================================================= +# SPDX-FileCopyrightText: 2021 Volker Krause +# +# SPDX-License-Identifier: BSD-3-Clause + +if (DEFINED QT_MAJOR_VERSION) + return() +endif() + +if (TARGET Qt5::Core) + set(QT_MAJOR_VERSION 5) +elseif (TARGET Qt6::Core) + set(QT_MAJOR_VERSION 6) +else() + option(BUILD_WITH_QT6 "Build against Qt 6" OFF) + + if (BUILD_WITH_QT6) + set(QT_MAJOR_VERSION 6) + else() + set(QT_MAJOR_VERSION 5) + endif() +endif() diff --git a/archived/projt-launcher/cmake/QtVersionlessBackport.cmake b/archived/projt-launcher/cmake/QtVersionlessBackport.cmake new file mode 100644 index 0000000000..46792db580 --- /dev/null +++ b/archived/projt-launcher/cmake/QtVersionlessBackport.cmake @@ -0,0 +1,97 @@ +#============================================================================= +# Copyright 2005-2011 Kitware, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Kitware, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +# From Qt5CoreMacros.cmake + +function(qt_generate_moc) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_generate_moc(${ARGV}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_generate_moc(${ARGV}) + endif() +endfunction() + +function(qt_wrap_cpp outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_wrap_cpp("${outfiles}" ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_wrap_cpp("${outfiles}" ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + +function(qt_add_binary_resources) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_add_binary_resources(${ARGV}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_add_binary_resources(${ARGV}) + endif() +endfunction() + +function(qt_add_resources outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_add_resources("${outfiles}" ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_add_resources("${outfiles}" ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + +function(qt_add_big_resources outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_add_big_resources(${outfiles} ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_add_big_resources(${outfiles} ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + +function(qt_import_plugins) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_import_plugins(${ARGV}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_import_plugins(${ARGV}) + endif() +endfunction() + + +# From Qt5WidgetsMacros.cmake + +function(qt_wrap_ui outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_wrap_ui("${outfiles}" ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_wrap_ui("${outfiles}" ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + diff --git a/archived/projt-launcher/cmake/findOpenSSLbyNuget.cmake b/archived/projt-launcher/cmake/findOpenSSLbyNuget.cmake new file mode 100644 index 0000000000..d4c8044b50 --- /dev/null +++ b/archived/projt-launcher/cmake/findOpenSSLbyNuget.cmake @@ -0,0 +1,34 @@ +function(projt_find_openssl_by_nuget) + # Find OpenSSL from NuGet + file(GLOB NUGET_OSSL_ROOTS "${CMAKE_SOURCE_DIR}/dependencies/openssl-native*") + if(NUGET_OSSL_ROOTS) + list(GET NUGET_OSSL_ROOTS 0 OSSL_ROOT_DIR) + message(STATUS "Found OpenSSL NuGet: ${OSSL_ROOT_DIR}") + set(OPENSSL_ROOT_DIR "${OSSL_ROOT_DIR}/build/native" CACHE PATH "" FORCE) + set(OPENSSL_USE_STATIC_LIBS ON) + + file(GLOB_RECURSE OSSL_LIBS "${OSSL_ROOT_DIR}/*.lib") + foreach(LIB_PATH ${OSSL_LIBS}) + # Strictly match architecture folder to avoid win-x86 + if(LIB_PATH MATCHES "win-${OSSL_ARCH}" OR LIB_PATH MATCHES "/${OSSL_ARCH}/") + if(LIB_PATH MATCHES "libcrypto.lib$|crypto.lib$") + if(NOT LIB_PATH MATCHES "[Dd]ebug" OR NOT OPENSSL_CRYPTO_LIBRARY) + set(OPENSSL_CRYPTO_LIBRARY "${LIB_PATH}" CACHE FILEPATH "" FORCE) + endif() + elseif(LIB_PATH MATCHES "libssl.lib$|ssl.lib$") + if(NOT LIB_PATH MATCHES "[Dd]ebug" OR NOT OPENSSL_SSL_LIBRARY) + set(OPENSSL_SSL_LIBRARY "${LIB_PATH}" CACHE FILEPATH "" FORCE) + endif() + endif() + endif() + endforeach() + + if(OPENSSL_CRYPTO_LIBRARY AND OPENSSL_SSL_LIBRARY) + set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/include" CACHE PATH "" FORCE) + set(OPENSSL_LIBRARIES "${OPENSSL_SSL_LIBRARY}" "${OPENSSL_CRYPTO_LIBRARY}" CACHE STRING "" FORCE) + message(STATUS "Found OpenSSL via NuGet: ${OPENSSL_CRYPTO_LIBRARY}") + set(OpenSSL_FOUND TRUE CACHE BOOL "" FORCE) + set(OPENSSL_FOUND TRUE CACHE BOOL "" FORCE) + endif() + endif() +endfunction() \ No newline at end of file diff --git a/archived/projt-launcher/cmake/useBZip2.cmake b/archived/projt-launcher/cmake/useBZip2.cmake new file mode 100644 index 0000000000..23a6d93723 --- /dev/null +++ b/archived/projt-launcher/cmake/useBZip2.cmake @@ -0,0 +1,45 @@ +function(projt_add_bzip2) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../bzip2/CMakeLists.txt") + message(STATUS "Using bundled bzip2") + set(ENABLE_LIB_ONLY ON CACHE BOOL "" FORCE) + set(ENABLE_TESTS OFF CACHE BOOL "" FORCE) + set(ENABLE_DOCS OFF CACHE BOOL "" FORCE) + set(ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE) + # Disable shared library and use static library for consistent linking + set(ENABLE_SHARED_LIB OFF CACHE BOOL "" FORCE) + set(ENABLE_STATIC_LIB ON CACHE BOOL "" FORCE) + # Disable instrumentation for external dependencies to avoid linker issues + string(REGEX REPLACE "-fprofile-instr-generate|--coverage|-fprofile-generate|-fprofile-arcs|-ftest-coverage" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE "-fprofile-instr-generate|--coverage|-fprofile-generate|-fprofile-arcs|-ftest-coverage" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + projt_push_output_dirs("bzip2") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../bzip2 bzip2) + projt_pop_output_dirs() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() + set(BZIP2_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../bzip2" CACHE PATH "" FORCE) + # Provide compatibility for CMake's FindBZip2 and consumers that expect + # BZip2 variables when using the bundled bzip2 implementation. + if(NOT TARGET BZip2::BZip2) + add_library(BZip2::BZip2 ALIAS bz2_static) + endif() + set(BZIP2_FOUND TRUE CACHE BOOL "" FORCE) + set(BZIP2_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../bzip2" CACHE PATH "" FORCE) + set(BZIP2_LIBRARIES bz2_static CACHE STRING "" FORCE) + else() + message(FATAL_ERROR "Bundled bzip2 not found") + endif() +endfunction() \ No newline at end of file diff --git a/archived/projt-launcher/cmake/useCMark.cmake b/archived/projt-launcher/cmake/useCMark.cmake new file mode 100644 index 0000000000..079dd9b544 --- /dev/null +++ b/archived/projt-launcher/cmake/useCMark.cmake @@ -0,0 +1,50 @@ +function(projt_add_cmark) + set(_launcher_prev_cmark_build_shared_libs "") + set(_launcher_cmark_build_shared_libs_set FALSE) + if(DEFINED BUILD_SHARED_LIBS) + set(_launcher_prev_cmark_build_shared_libs "${BUILD_SHARED_LIBS}") + set(_launcher_cmark_build_shared_libs_set TRUE) + endif() + set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(CMARK_SHARED OFF CACHE BOOL "" FORCE) + set(CMARK_STATIC ON CACHE BOOL "" FORCE) + set(_launcher_prev_cmark_osx_arch "${CMAKE_OSX_ARCHITECTURES}") + if(APPLE + AND CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "arm64|aarch64" + AND CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") + set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") + endif() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + projt_push_output_dirs("cmark") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cmark cmark) + projt_pop_output_dirs() + if(_launcher_cmark_build_shared_libs_set) + set(BUILD_SHARED_LIBS "${_launcher_prev_cmark_build_shared_libs}" CACHE BOOL "" FORCE) + else() + unset(BUILD_SHARED_LIBS CACHE) + endif() + unset(_launcher_prev_cmark_build_shared_libs) + unset(_launcher_cmark_build_shared_libs_set) + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() + if(DEFINED _launcher_prev_cmark_osx_arch) + set(CMAKE_OSX_ARCHITECTURES "${_launcher_prev_cmark_osx_arch}") + unset(_launcher_prev_cmark_osx_arch) + endif() + if(TARGET cmark AND NOT TARGET cmark::cmark) + add_library(cmark::cmark ALIAS cmark) + endif() +endfunction() \ No newline at end of file diff --git a/archived/projt-launcher/cmake/useLibpng.cmake b/archived/projt-launcher/cmake/useLibpng.cmake new file mode 100644 index 0000000000..ebb2d2034f --- /dev/null +++ b/archived/projt-launcher/cmake/useLibpng.cmake @@ -0,0 +1,48 @@ +function(projt_add_libpng) + # Ensure libpng install rules are enabled even if other deps toggled them. + set(SKIP_INSTALL_ALL OFF CACHE BOOL "" FORCE) + set(SKIP_INSTALL_LIBRARIES OFF CACHE BOOL "" FORCE) + set(PNG_SHARED ON CACHE BOOL "" FORCE) + set(PNG_STATIC OFF CACHE BOOL "" FORCE) + # Avoid loading both libpng16d and system libpng16 in Debug builds. + set(PNG_DEBUG_POSTFIX "" CACHE STRING "" FORCE) + set(PNG_TESTS OFF CACHE BOOL "" FORCE) + set(PNG_TOOLS OFF CACHE BOOL "" FORCE) + set(PNG_EXECUTABLES OFF CACHE BOOL "" FORCE) + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + projt_push_output_dirs("libpng") + projt_push_autogen_disabled() + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../libpng libpng) + projt_pop_autogen_disabled() + projt_pop_output_dirs() + foreach(_projt_png_target png_shared png_static png_framework) + if(TARGET ${_projt_png_target}) + target_compile_definitions( + ${_projt_png_target} + PRIVATE adler32=ptpng_adler32 + crc32=ptpng_crc32 + deflate=ptpng_deflate + deflateInit2_=ptpng_deflateInit2_ + deflateReset=ptpng_deflateReset + inflate=ptpng_inflate + inflateInit2_=ptpng_inflateInit2_ + inflateReset=ptpng_inflateReset + inflateReset2=ptpng_inflateReset2) + endif() + endforeach() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() +endfunction() \ No newline at end of file diff --git a/archived/projt-launcher/cmake/useLibqrencode.cmake b/archived/projt-launcher/cmake/useLibqrencode.cmake new file mode 100644 index 0000000000..268ff019f2 --- /dev/null +++ b/archived/projt-launcher/cmake/useLibqrencode.cmake @@ -0,0 +1,91 @@ +function(projt_add_libqrencode) + # Avoid optional tools/tests that require getopt/PNG on Windows. + if(WIN32) + set(_qrencode_prev_build_shared_libs "${BUILD_SHARED_LIBS}") + set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(ENABLE_SHARED OFF CACHE BOOL "" FORCE) + set(ENABLE_STATIC ON CACHE BOOL "" FORCE) + endif() + set(WITH_TOOLS OFF CACHE BOOL "" FORCE) + set(WITH_PNG OFF CACHE BOOL "" FORCE) + set(_launcher_prev_build_testing "${BUILD_TESTING}") + set(_launcher_prev_with_tests "") + set(_launcher_with_tests_set FALSE) + if(DEFINED WITH_TESTS) + set(_launcher_prev_with_tests "${WITH_TESTS}") + set(_launcher_with_tests_set TRUE) + endif() + set(BUILD_TESTING OFF CACHE BOOL "" FORCE) + set(WITH_TESTS OFF CACHE BOOL "" FORCE) + set(_qrencode_prev_install_libdir "") + set(_qrencode_install_libdir_set FALSE) + if(APPLE) + set(_qrencode_bundle_dir "ProjTLauncher.app/Contents/Frameworks") + if(DEFINED Launcher_Name AND NOT Launcher_Name STREQUAL "") + set(_qrencode_bundle_dir "${Launcher_Name}.app/Contents/Frameworks") + endif() + if(DEFINED CMAKE_INSTALL_LIBDIR) + set(_qrencode_prev_install_libdir "${CMAKE_INSTALL_LIBDIR}") + set(_qrencode_install_libdir_set TRUE) + endif() + set(CMAKE_INSTALL_LIBDIR "${_qrencode_bundle_dir}" CACHE PATH "" FORCE) + endif() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + projt_push_output_dirs("libqrencode") + projt_push_autogen_disabled() + set(PROJT_DISABLE_SUMMARY ON CACHE BOOL "" FORCE) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode libqrencode) + projt_pop_autogen_disabled() + projt_pop_output_dirs() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() + if(APPLE) + if(_qrencode_install_libdir_set) + set(CMAKE_INSTALL_LIBDIR "${_qrencode_prev_install_libdir}" CACHE PATH "" FORCE) + else() + unset(CMAKE_INSTALL_LIBDIR CACHE) + endif() + endif() + if(WIN32) + set(BUILD_SHARED_LIBS "${_qrencode_prev_build_shared_libs}" CACHE BOOL "" FORCE) + endif() + set(BUILD_TESTING "${_launcher_prev_build_testing}" CACHE BOOL "" FORCE) + if(_launcher_with_tests_set) + set(WITH_TESTS "${_launcher_prev_with_tests}" CACHE BOOL "" FORCE) + else() + unset(WITH_TESTS CACHE) + endif() + unset(_launcher_prev_build_testing) + unset(_launcher_prev_with_tests) + unset(_launcher_with_tests_set) + unset(_qrencode_prev_install_libdir) + unset(_qrencode_install_libdir_set) + add_library(qrcodegenerator ALIAS qrencode) + # Ensure the qrencode target exposes include directories so consumers can find qrencode.h + if(TARGET qrencode) + get_target_property(_qr_inc qrencode INTERFACE_INCLUDE_DIRECTORIES) + if(NOT _qr_inc) + target_include_directories(qrencode PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode" + "${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode/src" + "${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode/lib" + "${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode/include") + endif() + endif() + if(NOT DEFINED QRENCODE_INC OR QRENCODE_INC STREQUAL "") + set(QRENCODE_INC "${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode;${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode/include;${CMAKE_CURRENT_SOURCE_DIR}/../libqrencode/lib" CACHE PATH "QRENCODE include dir" FORCE) + endif() +endfunction() diff --git a/archived/projt-launcher/cmake/useMinimalLibs.cmake b/archived/projt-launcher/cmake/useMinimalLibs.cmake new file mode 100644 index 0000000000..b7f7f4455a --- /dev/null +++ b/archived/projt-launcher/cmake/useMinimalLibs.cmake @@ -0,0 +1,60 @@ +function(projt_add_internallibs) + option(NBT_BUILD_SHARED "Build NBT shared library" OFF) + option(NBT_USE_ZLIB "Build NBT library with zlib support" ON) + option(NBT_BUILD_TESTS "Build NBT library tests" OFF) + projt_push_output_dirs("libnbtplusplus") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../libnbtplusplus libnbtplusplus) + projt_pop_output_dirs() + + if(NOT LAUNCHER_FUZZ_ONLY) + projt_push_output_dirs("systeminfo") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../systeminfo systeminfo) # system information library + projt_pop_output_dirs() + endif() + if(NOT LAUNCHER_FUZZ_ONLY) + projt_push_output_dirs("launcherjava") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../javaloader javaloader) # java based launcher part for Minecraft + projt_pop_output_dirs() + projt_push_output_dirs("javacheck") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../javacheck javacheck) # java compatibility checker + projt_pop_output_dirs() + projt_push_output_dirs("rainbow") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../rainbow rainbow) # Qt extension for colors + projt_pop_output_dirs() + projt_push_output_dirs("LocalPeer") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../LocalPeer LocalPeer) # fork of a library from Qt solutions + projt_pop_output_dirs() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + if(DEFINED Launcher_BUNDLED_LIBEXECDIR AND NOT Launcher_BUNDLED_LIBEXECDIR STREQUAL "") + projt_push_install_libexecdir("${Launcher_BUNDLED_LIBEXECDIR}") + endif() + endif() + set(GAMEMODE_WITH_PRIVILEGED_GROUP "ptgamemode" CACHE STRING "" FORCE) + projt_push_output_dirs("gamemode") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../gamemode gamemode) + projt_pop_output_dirs() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBEXECDIR AND NOT Launcher_BUNDLED_LIBEXECDIR STREQUAL "") + projt_pop_install_libexecdir() + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() + projt_push_output_dirs("murmur2") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../murmur2 murmur2) # Hash for usage with the CurseForge API + projt_pop_output_dirs() + projt_push_output_dirs("qdcss") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../qdcss qdcss) # css parser + projt_pop_output_dirs() + endif() +endfunction() \ No newline at end of file diff --git a/archived/projt-launcher/cmake/useMinizip.cmake b/archived/projt-launcher/cmake/useMinizip.cmake new file mode 100644 index 0000000000..6091a9429e --- /dev/null +++ b/archived/projt-launcher/cmake/useMinizip.cmake @@ -0,0 +1,26 @@ +function(projt_add_minizip) + set(MINIZIP_BUILD_SHARED ON CACHE BOOL "" FORCE) + set(MINIZIP_BUILD_STATIC OFF CACHE BOOL "" FORCE) + set(MINIZIP_BUILD_TESTING OFF CACHE BOOL "" FORCE) + set(MINIZIP_INSTALL ON CACHE BOOL "" FORCE) + set(MINIZIP_ENABLE_BZIP2 ON CACHE BOOL "" FORCE) + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + projt_push_output_dirs("minizip") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../ptlibzippy/contrib/minizip minizip) + projt_pop_output_dirs() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() +endfunction() \ No newline at end of file diff --git a/archived/projt-launcher/cmake/usePTlibzippy.cmake b/archived/projt-launcher/cmake/usePTlibzippy.cmake new file mode 100644 index 0000000000..766baf615a --- /dev/null +++ b/archived/projt-launcher/cmake/usePTlibzippy.cmake @@ -0,0 +1,96 @@ +function(projt_add_ptlibzippy) + set(_projt_prev_skip_install_all "${SKIP_INSTALL_ALL}") + set(SKIP_INSTALL_ALL ON CACHE BOOL "" FORCE) + + # Fuzzing + if(BUILD_FUZZERS) + set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(PTLIBZIPPY_BUILD_SHARED OFF CACHE BOOL "" FORCE) + set(PTLIBZIPPY_BUILD_STATIC ON CACHE BOOL "" FORCE) + message(STATUS "Fuzzing build: Building ptlibzippy as static library only") + endif() + + # Save flags + set(_SAVED_CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + set(_SAVED_CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + set(_SAVED_CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") + set(_SAVED_CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") + + # Strip coverage + string(REGEX REPLACE "-fprofile-instr-generate|--coverage|-fprofile-generate|-fprofile-arcs|-ftest-coverage" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE "-fprofile-instr-generate|--coverage|-fprofile-generate|-fprofile-arcs|-ftest-coverage" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + + # Shared/static logic + if(UNIX AND NOT APPLE AND NOT BUILD_FUZZERS) + set(PTLIBZIPPY_BUILD_SHARED ON CACHE BOOL "" FORCE) + set(PTLIBZIPPY_BUILD_STATIC ON CACHE BOOL "" FORCE) + endif() + + set(PTLIBZIPPY_PREFIX OFF CACHE BOOL "" FORCE) + + # OSS-Fuzz sanitizer strip + if(DEFINED ENV{LIB_FUZZING_ENGINE}) + string(REGEX REPLACE "-fsanitize=[^ ]*" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE "-fsanitize-coverage=[^ ]*" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE "-fno-sanitize-recover=[^ ]*" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + message(STATUS "OSS-Fuzz build: Stripping sanitizer flags from zlib") + endif() + + # Install dirs + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + + projt_push_output_dirs("zlib") + projt_push_autogen_disabled() + + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../ptlibzippy ptlibzippy) + + projt_pop_autogen_disabled() + projt_pop_output_dirs() + + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() + + # Restore flags + set(CMAKE_C_FLAGS "${_SAVED_CMAKE_C_FLAGS}") + set(CMAKE_CXX_FLAGS "${_SAVED_CMAKE_CXX_FLAGS}") + set(CMAKE_C_FLAGS_DEBUG "${_SAVED_CMAKE_C_FLAGS_DEBUG}") + set(CMAKE_C_FLAGS_RELEASE "${_SAVED_CMAKE_C_FLAGS_RELEASE}") + + # PIC fix + if(TARGET ptlibzippystatic) + set_target_properties(ptlibzippystatic PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() + if(TARGET ptlibzippy) + set_target_properties(ptlibzippy PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() + + # Export variables + if(TARGET PTlibzippy::PTlibzippy) + get_target_property(_projt_ptlibzippy_includes PTlibzippy::PTlibzippy INTERFACE_INCLUDE_DIRECTORIES) + endif() + + if(_projt_ptlibzippy_includes) + set(PTLIBZIPPY_INCLUDE_DIR "${_projt_ptlibzippy_includes}" PARENT_SCOPE) + elseif(DEFINED PTlibzippy_SOURCE_DIR AND DEFINED PTlibzippy_BINARY_DIR) + set(PTLIBZIPPY_INCLUDE_DIR "${PTlibzippy_SOURCE_DIR};${PTlibzippy_BINARY_DIR}" PARENT_SCOPE) + endif() + + set(PTLIBZIPPY_LIBRARIES "PTlibzippy::PTlibzippy" PARENT_SCOPE) + + unset(_projt_ptlibzippy_includes) + + set(SKIP_INSTALL_ALL "${_projt_prev_skip_install_all}" CACHE BOOL "" FORCE) +endfunction() diff --git a/archived/projt-launcher/cmake/useQuazip.cmake b/archived/projt-launcher/cmake/useQuazip.cmake new file mode 100644 index 0000000000..d80374304a --- /dev/null +++ b/archived/projt-launcher/cmake/useQuazip.cmake @@ -0,0 +1,31 @@ +function(projt_add_quazip) + set(QUAZIP_ENABLE_TESTS OFF CACHE BOOL "" FORCE) + set(QUAZIP_ENABLE_QTEXTCODEC OFF CACHE BOOL "" FORCE) + set(QUAZIP_INSTALL ON CACHE BOOL "" FORCE) + set(QUAZIP_INSTALL_CMAKE_CONFIG OFF CACHE BOOL "" FORCE) + set(QUAZIP_BZIP2 ON CACHE BOOL "" FORCE) + set(QUAZIP_FETCH_LIBS OFF CACHE BOOL "" FORCE) + set(QUAZIP_FORCE_FETCH_LIBS OFF CACHE BOOL "" FORCE) + # Disable instrumentation for external dependencies to avoid linker issues + string(REGEX REPLACE "-fprofile-instr-generate|--coverage|-fprofile-generate|-fprofile-arcs|-ftest-coverage" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + string(REGEX REPLACE "-fprofile-instr-generate|--coverage|-fprofile-generate|-fprofile-arcs|-ftest-coverage" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + projt_push_output_dirs("quazip") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../quazip quazip) + projt_pop_output_dirs() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() +endfunction() diff --git a/archived/projt-launcher/cmake/useTomlplusplus.cmake b/archived/projt-launcher/cmake/useTomlplusplus.cmake new file mode 100644 index 0000000000..6d684a032f --- /dev/null +++ b/archived/projt-launcher/cmake/useTomlplusplus.cmake @@ -0,0 +1,21 @@ +function(projt_add_tomlplusplus) + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_push_install_libdir("${Launcher_BUNDLED_LIBDIR}") + endif() + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_push_install_includedir("${Launcher_BUNDLED_INCLUDEDIR}") + endif() + endif() + projt_push_output_dirs("tomlplusplus") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../tomlplusplus tomlplusplus) + projt_pop_output_dirs() + if(UNIX AND NOT APPLE) + if(DEFINED Launcher_BUNDLED_INCLUDEDIR AND NOT Launcher_BUNDLED_INCLUDEDIR STREQUAL "") + projt_pop_install_includedir() + endif() + if(DEFINED Launcher_BUNDLED_LIBDIR AND NOT Launcher_BUNDLED_LIBDIR STREQUAL "") + projt_pop_install_libdir() + endif() + endif() +endfunction() \ No newline at end of file diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/README.md b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/README.md new file mode 100644 index 0000000000..12dc29efc0 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/README.md @@ -0,0 +1,3 @@ +The only difference between this and the upstream vcpkg port is the addition of `universal-osx.patch`. It's very annoying we need to bundle this entire tree to do that. + +-@YongDo-Hyun diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch new file mode 100644 index 0000000000..ad800aa66e --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch @@ -0,0 +1,13 @@ +diff --git a/mesonbuild/cmake/toolchain.py b/mesonbuild/cmake/toolchain.py +index 11a00be5d..89ae490ff 100644 +--- a/mesonbuild/cmake/toolchain.py ++++ b/mesonbuild/cmake/toolchain.py +@@ -202,7 +202,7 @@ class CMakeToolchain: + @staticmethod + def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool: + if compiler.get_argument_syntax() == 'msvc': +- return arg.startswith('/') ++ return arg.startswith(('/','-')) + else: + if os.path.basename(compiler.get_exe()) == 'zig' and arg in {'ar', 'cc', 'c++', 'dlltool', 'lib', 'ranlib', 'objcopy', 'rc'}: + return True diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch new file mode 100644 index 0000000000..0cbfe717de --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch @@ -0,0 +1,45 @@ +diff --git a/mesonbuild/dependencies/python.py b/mesonbuild/dependencies/python.py +index 883a29a..d9a82af 100644 +--- a/mesonbuild/dependencies/python.py ++++ b/mesonbuild/dependencies/python.py +@@ -232,8 +232,10 @@ class _PythonDependencyBase(_Base): + else: + if self.is_freethreaded: + libpath = Path('libs') / f'python{vernum}t.lib' ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}t.lib' + else: + libpath = Path('libs') / f'python{vernum}.lib' ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}.lib' + # For a debug build, pyconfig.h may force linking with + # pythonX_d.lib (see meson#10776). This cannot be avoided + # and won't work unless we also have a debug build of +@@ -250,6 +252,8 @@ class _PythonDependencyBase(_Base): + vscrt = self.env.coredata.optstore.get_value('b_vscrt') + if vscrt in {'mdd', 'mtd', 'from_buildtype', 'static_from_buildtype'}: + vscrt_debug = True ++ if is_debug_build: ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'debug/lib' / f'python{vernum}_d.lib' + if is_debug_build and vscrt_debug and not self.variables.get('Py_DEBUG'): + mlog.warning(textwrap.dedent('''\ + Using a debug build type with MSVC or an MSVC-compatible compiler +@@ -350,9 +354,10 @@ class PythonSystemDependency(SystemDependency, _PythonDependencyBase): + self.is_found = True + + # compile args ++ verdot = self.variables.get('py_version_short') + inc_paths = mesonlib.OrderedSet([ + self.variables.get('INCLUDEPY'), +- self.paths.get('include'), ++ self.paths.get('include') + f'/../../../include/python${verdot}', + self.paths.get('platinclude')]) + + self.compile_args += ['-I' + path for path in inc_paths if path] +@@ -416,7 +421,7 @@ def python_factory(env: 'Environment', for_machine: 'MachineChoice', + candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) + # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, + # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. +- if pkg_libdir is not None: ++ if True or pkg_libdir is not None: + candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) + else: + candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch new file mode 100644 index 0000000000..394b064dc4 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch @@ -0,0 +1,52 @@ +From a16ec8b0fb6d7035b669a13edd4d97ff0c307a0b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Martin=20D=C3=B8rum?= +Date: Fri, 2 May 2025 10:56:28 +0200 +Subject: [PATCH] cpp: fix _LIBCPP_ENABLE_ASSERTIONS warning + +libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS from version 18. +However, the libc++ shipped with Apple Clang backported that +deprecation in version 17 already, +which is the version which Apple currently ships for macOS. +This PR changes the _LIBCPP_ENABLE_ASSERTIONS deprecation check +to use version ">=17" on Apple Clang. +--- + mesonbuild/compilers/cpp.py | 12 ++++++++++-- + 1 file changed, 10 insertions(+), 2 deletions(-) + +diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py +index 01b9bb9fa34f..f7dc150e8608 100644 +--- a/mesonbuild/compilers/cpp.py ++++ b/mesonbuild/compilers/cpp.py +@@ -311,6 +311,9 @@ def get_option_link_args(self, target: 'BuildTarget', env: 'Environment', subpro + return libs + return [] + ++ def is_libcpp_enable_assertions_deprecated(self) -> bool: ++ return version_compare(self.version, ">=18") ++ + def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: + if disable: + return ['-DNDEBUG'] +@@ -323,7 +326,7 @@ def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: + if self.language_stdlib_provider(env) == 'stdc++': + return ['-D_GLIBCXX_ASSERTIONS=1'] + else: +- if version_compare(self.version, '>=18'): ++ if self.is_libcpp_enable_assertions_deprecated(): + return ['-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST'] + elif version_compare(self.version, '>=15'): + return ['-D_LIBCPP_ENABLE_ASSERTIONS=1'] +@@ -343,7 +346,12 @@ class ArmLtdClangCPPCompiler(ClangCPPCompiler): + + + class AppleClangCPPCompiler(AppleCompilerMixin, AppleCPPStdsMixin, ClangCPPCompiler): +- pass ++ def is_libcpp_enable_assertions_deprecated(self) -> bool: ++ # Upstream libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS ++ # in favor of _LIBCPP_HARDENING_MODE from version 18 onwards, ++ # but Apple Clang 17's libc++ has back-ported that change. ++ # See: https://github.com/mesonbuild/meson/issues/14440 ++ return version_compare(self.version, ">=17") + + + class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler): diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake new file mode 100644 index 0000000000..84201aa1aa --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake @@ -0,0 +1,5 @@ +file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/tools/meson") +file(INSTALL "${SOURCE_PATH}/meson.py" + "${SOURCE_PATH}/mesonbuild" + DESTINATION "${CURRENT_PACKAGES_DIR}/tools/meson" +) diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch new file mode 100644 index 0000000000..8f2a029de5 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch @@ -0,0 +1,13 @@ +diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py +--- a/mesonbuild/dependencies/misc.py ++++ b/mesonbuild/dependencies/misc.py +@@ -593,7 +593,8 @@ iconv_factory = DependencyFactory( + + packages['intl'] = intl_factory = DependencyFactory( + 'intl', ++ [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], ++ cmake_name='Intl', +- [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], + builtin_class=IntlBuiltinDependency, + system_class=IntlSystemDependency, + ) diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in new file mode 100644 index 0000000000..df21b753b0 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in @@ -0,0 +1,43 @@ +[binaries] +cmake = ['@CMAKE_COMMAND@'] +ninja = ['@NINJA@'] +pkg-config = ['@PKGCONFIG@'] +@MESON_MT@ +@MESON_AR@ +@MESON_RC@ +@MESON_C@ +@MESON_C_LD@ +@MESON_CXX@ +@MESON_CXX_LD@ +@MESON_OBJC@ +@MESON_OBJC_LD@ +@MESON_OBJCPP@ +@MESON_OBJCPP_LD@ +@MESON_FC@ +@MESON_FC_LD@ +@MESON_WINDRES@ +@MESON_ADDITIONAL_BINARIES@ +[properties] +cmake_toolchain_file = '@SCRIPTS@/buildsystems/vcpkg.cmake' +@MESON_ADDITIONAL_PROPERTIES@ +[cmake] +CMAKE_BUILD_TYPE = '@MESON_CMAKE_BUILD_TYPE@' +VCPKG_TARGET_TRIPLET = '@TARGET_TRIPLET@' +VCPKG_HOST_TRIPLET = '@_HOST_TRIPLET@' +VCPKG_CHAINLOAD_TOOLCHAIN_FILE = '@VCPKG_CHAINLOAD_TOOLCHAIN_FILE@' +VCPKG_CRT_LINKAGE = '@VCPKG_CRT_LINKAGE@' +_VCPKG_INSTALLED_DIR = '@_VCPKG_INSTALLED_DIR@' +@MESON_HOST_MACHINE@ +@MESON_BUILD_MACHINE@ +[built-in options] +default_library = '@MESON_DEFAULT_LIBRARY@' +werror = false +@MESON_CFLAGS@ +@MESON_CXXFLAGS@ +@MESON_FCFLAGS@ +@MESON_OBJCFLAGS@ +@MESON_OBJCPPFLAGS@ +# b_vscrt +@MESON_VSCRT_LINKAGE@ +# c_winlibs/cpp_winlibs +@MESON_WINLIBS@ \ No newline at end of file diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake new file mode 100644 index 0000000000..9d09373ebd --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake @@ -0,0 +1,45 @@ +# This port represents a dependency on the Meson build system. +# In the future, it is expected that this port acquires and installs Meson. +# Currently is used in ports that call vcpkg_find_acquire_program(MESON) in order to force rebuilds. + +set(VCPKG_POLICY_CMAKE_HELPER_PORT enabled) + +set(patches + meson-intl.patch + adjust-python-dep.patch + adjust-args.patch + remove-freebsd-pcfile-specialization.patch + fix-libcpp-enable-assertions.patch # https://github.com/mesonbuild/meson/pull/14548, Remove in 1.8.3 + universal-osx.patch # NOTE(@YongDo-Hyun): THIS IS THE ONLY CHANGE NEEDED FOR PROJT +) +set(scripts + vcpkg-port-config.cmake + vcpkg_configure_meson.cmake + vcpkg_install_meson.cmake + meson.template.in +) +set(to_hash + "${CMAKE_CURRENT_LIST_DIR}/vcpkg.json" + "${CMAKE_CURRENT_LIST_DIR}/portfile.cmake" +) +foreach(file IN LISTS patches scripts) + set(filepath "${CMAKE_CURRENT_LIST_DIR}/${file}") + list(APPEND to_hash "${filepath}") + file(COPY "${filepath}" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +endforeach() + +set(meson_path_hash "") +foreach(filepath IN LISTS to_hash) + file(SHA1 "${filepath}" to_append) + string(APPEND meson_path_hash "${to_append}") +endforeach() +string(SHA512 meson_path_hash "${meson_path_hash}") + +string(SUBSTRING "${meson_path_hash}" 0 6 MESON_SHORT_HASH) +list(TRANSFORM patches REPLACE [[^(..*)$]] [["${CMAKE_CURRENT_LIST_DIR}/\0"]]) +list(JOIN patches "\n " PATCHES) +configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-port-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake" @ONLY) + +vcpkg_install_copyright(FILE_LIST "${VCPKG_ROOT_DIR}/LICENSE.txt") + +include("${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake") diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch new file mode 100644 index 0000000000..947345ccf8 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch @@ -0,0 +1,23 @@ +diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py +index cc0450a52..13501466d 100644 +--- a/mesonbuild/modules/pkgconfig.py ++++ b/mesonbuild/modules/pkgconfig.py +@@ -701,16 +701,8 @@ class PkgConfigModule(NewExtensionModule): + pcfile = filebase + '.pc' + pkgroot = pkgroot_name = kwargs['install_dir'] or default_install_dir + if pkgroot is None: +- m = state.environment.machines.host +- if m.is_freebsd(): +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'libdata', 'pkgconfig') +- pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') +- elif m.is_haiku(): +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig') +- pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig') +- else: +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') +- pkgroot_name = os.path.join('{libdir}', 'pkgconfig') ++ pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') ++ pkgroot_name = os.path.join('{libdir}', 'pkgconfig') + relocatable = state.get_option('pkgconfig.relocatable') + self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, + version, pcfile, conflicts, variables, diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch new file mode 100644 index 0000000000..58b96d5ce4 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch @@ -0,0 +1,16 @@ +diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py +index f57957f0b..a72e72a0b 100644 +--- a/mesonbuild/compilers/detect.py ++++ b/mesonbuild/compilers/detect.py +@@ -1472,6 +1472,11 @@ def _get_clang_compiler_defines(compiler: T.List[str], lang: str) -> T.Dict[str, + """ + from .mixins.clang import clang_lang_map + ++ # Filter out `-arch` flags passed to the compiler for Universal Binaries ++ # https://github.com/mesonbuild/meson/issues/5290 ++ # https://github.com/mesonbuild/meson/issues/8206 ++ compiler = [arg for i, arg in enumerate(compiler) if not (i > 0 and compiler[i - 1] == "-arch") and not arg == "-arch"] ++ + def _try_obtain_compiler_defines(args: T.List[str]) -> str: + mlog.debug(f'Running command: {join_args(args)}') + p, output, error = Popen_safe(compiler + args, write='', stdin=subprocess.PIPE) diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake new file mode 100644 index 0000000000..c0dee3a382 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake @@ -0,0 +1,62 @@ +include("${CURRENT_HOST_INSTALLED_DIR}/share/vcpkg-cmake-get-vars/vcpkg-port-config.cmake") +# Overwrite builtin scripts +include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_configure_meson.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_install_meson.cmake") + +set(meson_short_hash @MESON_SHORT_HASH@) + +# Setup meson: +set(program MESON) +set(program_version @VERSION@) +set(program_name meson) +set(search_names meson meson.py) +set(ref "${program_version}") +set(path_to_search "${DOWNLOADS}/tools/meson-${program_version}-${meson_short_hash}") +set(download_urls "https://github.com/mesonbuild/meson/archive/${ref}.tar.gz") +set(download_filename "meson-${ref}.tar.gz") +set(download_sha512 bd2e65f0863d9cb974e659ff502d773e937b8a60aaddfd7d81e34cd2c296c8e82bf214d790ac089ba441543059dfc2677ba95ed51f676df9da420859f404a907) + +find_program(SCRIPT_MESON NAMES ${search_names} PATHS "${path_to_search}" NO_DEFAULT_PATH) # NO_DEFAULT_PATH due top patching + +if(NOT SCRIPT_MESON) + vcpkg_download_distfile(archive_path + URLS ${download_urls} + SHA512 "${download_sha512}" + FILENAME "${download_filename}" + ) + file(REMOVE_RECURSE "${path_to_search}") + file(REMOVE_RECURSE "${path_to_search}-tmp") + file(MAKE_DIRECTORY "${path_to_search}-tmp") + file(ARCHIVE_EXTRACT INPUT "${archive_path}" + DESTINATION "${path_to_search}-tmp" + #PATTERNS "**/mesonbuild/*" "**/*.py" + ) + z_vcpkg_apply_patches( + SOURCE_PATH "${path_to_search}-tmp/meson-${ref}" + PATCHES + @PATCHES@ + ) + file(MAKE_DIRECTORY "${path_to_search}") + file(RENAME "${path_to_search}-tmp/meson-${ref}/meson.py" "${path_to_search}/meson.py") + file(RENAME "${path_to_search}-tmp/meson-${ref}/mesonbuild" "${path_to_search}/mesonbuild") + file(REMOVE_RECURSE "${path_to_search}-tmp") + set(SCRIPT_MESON "${path_to_search}/meson.py") +endif() + +# Check required python version +vcpkg_find_acquire_program(PYTHON3) +vcpkg_execute_in_download_mode( + COMMAND "${PYTHON3}" --version + OUTPUT_VARIABLE version_contents + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}" +) +string(REGEX MATCH [[[0-9]+\.[0-9]+\.[0-9]+]] python_ver "${version_contents}") + +set(min_required 3.7) +if(python_ver VERSION_LESS "${min_required}") + message(FATAL_ERROR "Found Python version '${python_ver} at ${PYTHON3}' is insufficient for meson. meson requires at least version '${min_required}'") +else() + message(STATUS "Found Python version '${python_ver} at ${PYTHON3}'") +endif() + +message(STATUS "Using meson: ${SCRIPT_MESON}") diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json new file mode 100644 index 0000000000..04a0cbbec8 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json @@ -0,0 +1,11 @@ +{ + "name": "vcpkg-tool-meson", + "version": "1.8.2", + "description": "Meson build system", + "homepage": "https://github.com/mesonbuild/meson", + "license": "Apache-2.0", + "supports": "native", + "dependencies": [ + "vcpkg-cmake-get-vars" + ] +} diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake new file mode 100644 index 0000000000..6b00200d18 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake @@ -0,0 +1,480 @@ +function(z_vcpkg_meson_set_proglist_variables config_type) + if(VCPKG_TARGET_IS_WINDOWS) + set(proglist MT AR) + else() + set(proglist AR RANLIB STRIP NM OBJDUMP DLLTOOL MT) + endif() + foreach(prog IN LISTS proglist) + if(VCPKG_DETECTED_CMAKE_${prog}) + if(meson_${prog}) + string(TOUPPER "MESON_${meson_${prog}}" var_to_set) + set("${var_to_set}" "${meson_${prog}} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) + elseif(${prog} STREQUAL AR AND VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}) + # Probably need to move AR somewhere else + string(TOLOWER "${prog}" proglower) + z_vcpkg_meson_convert_compiler_flags_to_list(ar_flags "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}}") + list(PREPEND ar_flags "${VCPKG_DETECTED_CMAKE_${prog}}") + z_vcpkg_meson_convert_list_to_python_array(ar_flags ${ar_flags}) + set("MESON_AR" "${proglower} = ${ar_flags}" PARENT_SCOPE) + else() + string(TOUPPER "MESON_${prog}" var_to_set) + string(TOLOWER "${prog}" proglower) + set("${var_to_set}" "${proglower} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) + endif() + endif() + endforeach() + set(compilers "${arg_LANGUAGES}") + if(VCPKG_TARGET_IS_WINDOWS) + list(APPEND compilers RC) + endif() + set(meson_RC windres) + set(meson_Fortran fortran) + set(meson_CXX cpp) + foreach(prog IN LISTS compilers) + if(VCPKG_DETECTED_CMAKE_${prog}_COMPILER) + string(TOUPPER "MESON_${prog}" var_to_set) + if(meson_${prog}) + if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) + # Need compiler flags in prog vars for sanity check. + z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") + endif() + list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") + list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise + z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) + set("${var_to_set}" "${meson_${prog}} = ${${prog}flags}" PARENT_SCOPE) + if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID + AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" + AND VCPKG_DETECTED_CMAKE_LINKER) + string(TOUPPER "MESON_${prog}_LD" var_to_set) + set(${var_to_set} "${meson_${prog}}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) + endif() + else() + if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) + # Need compiler flags in prog vars for sanity check. + z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") + endif() + list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") + list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise + z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) + string(TOLOWER "${prog}" proglower) + set("${var_to_set}" "${proglower} = ${${prog}flags}" PARENT_SCOPE) + if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID + AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" + AND VCPKG_DETECTED_CMAKE_LINKER) + string(TOUPPER "MESON_${prog}_LD" var_to_set) + set(${var_to_set} "${proglower}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) + endif() + endif() + endif() + endforeach() +endfunction() + +function(z_vcpkg_meson_convert_compiler_flags_to_list out_var compiler_flags) + separate_arguments(cmake_list NATIVE_COMMAND "${compiler_flags}") + list(TRANSFORM cmake_list REPLACE ";" [[\\;]]) + set("${out_var}" "${cmake_list}" PARENT_SCOPE) +endfunction() + +function(z_vcpkg_meson_convert_list_to_python_array out_var) + z_vcpkg_function_arguments(flag_list 1) + vcpkg_list(REMOVE_ITEM flag_list "") # remove empty elements if any + vcpkg_list(JOIN flag_list "', '" flag_list) + set("${out_var}" "['${flag_list}']" PARENT_SCOPE) +endfunction() + +# Generates the required compiler properties for meson +function(z_vcpkg_meson_set_flags_variables config_type) + if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + set(libpath_flag /LIBPATH:) + else() + set(libpath_flag -L) + endif() + if(config_type STREQUAL "DEBUG") + set(path_suffix "/debug") + else() + set(path_suffix "") + endif() + + set(includepath "-I${CURRENT_INSTALLED_DIR}/include") + set(libpath "${libpath_flag}${CURRENT_INSTALLED_DIR}${path_suffix}/lib") + + foreach(lang IN LISTS arg_LANGUAGES) + z_vcpkg_meson_convert_compiler_flags_to_list(${lang}flags "${VCPKG_COMBINED_${lang}_FLAGS_${config_type}}") + if(lang MATCHES "^(C|CXX)$") + vcpkg_list(APPEND ${lang}flags "${includepath}") + endif() + z_vcpkg_meson_convert_list_to_python_array(${lang}flags ${${lang}flags}) + set(lang_mapping "${lang}") + if(lang STREQUAL "Fortran") + set(lang_mapping "FC") + endif() + string(TOLOWER "${lang_mapping}" langlower) + if(lang STREQUAL "CXX") + set(langlower cpp) + endif() + set(MESON_${lang_mapping}FLAGS "${langlower}_args = ${${lang}flags}\n") + set(linker_flags "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_${config_type}}") + z_vcpkg_meson_convert_compiler_flags_to_list(linker_flags "${linker_flags}") + vcpkg_list(APPEND linker_flags "${libpath}") + z_vcpkg_meson_convert_list_to_python_array(linker_flags ${linker_flags}) + string(APPEND MESON_${lang_mapping}FLAGS "${langlower}_link_args = ${linker_flags}\n") + set(MESON_${lang_mapping}FLAGS "${MESON_${lang_mapping}FLAGS}" PARENT_SCOPE) + endforeach() +endfunction() + +function(z_vcpkg_get_build_and_host_system build_system host_system is_cross) #https://mesonbuild.com/Cross-compilation.html + set(build_unknown FALSE) + if(CMAKE_HOST_WIN32) + if(DEFINED ENV{PROCESSOR_ARCHITEW6432}) + set(build_arch $ENV{PROCESSOR_ARCHITEW6432}) + else() + set(build_arch $ENV{PROCESSOR_ARCHITECTURE}) + endif() + if(build_arch MATCHES "(amd|AMD)64") + set(build_cpu_fam x86_64) + set(build_cpu x86_64) + elseif(build_arch MATCHES "(x|X)86") + set(build_cpu_fam x86) + set(build_cpu i686) + elseif(build_arch MATCHES "^(ARM|arm)64$") + set(build_cpu_fam aarch64) + set(build_cpu armv8) + elseif(build_arch MATCHES "^(ARM|arm)$") + set(build_cpu_fam arm) + set(build_cpu armv7hl) + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unsupported build architecture ${build_arch}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + elseif(CMAKE_HOST_UNIX) + # at this stage, CMAKE_HOST_SYSTEM_PROCESSOR is not defined + execute_process( + COMMAND uname -m + OUTPUT_VARIABLE MACHINE + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) + + # Show real machine architecture to visually understand whether we are in a native Apple Silicon terminal or running under Rosetta emulation + debug_message("Machine: ${MACHINE}") + + if(MACHINE MATCHES "arm64|aarch64") + set(build_cpu_fam aarch64) + set(build_cpu armv8) + elseif(MACHINE MATCHES "armv7h?l") + set(build_cpu_fam arm) + set(build_cpu ${MACHINE}) + elseif(MACHINE MATCHES "x86_64|amd64") + set(build_cpu_fam x86_64) + set(build_cpu x86_64) + elseif(MACHINE MATCHES "x86|i686") + set(build_cpu_fam x86) + set(build_cpu i686) + elseif(MACHINE MATCHES "i386") + set(build_cpu_fam x86) + set(build_cpu i386) + elseif(MACHINE MATCHES "loongarch64") + set(build_cpu_fam loongarch64) + set(build_cpu loongarch64) + else() + # https://github.com/mesonbuild/meson/blob/master/docs/markdown/Reference-tables.md#cpu-families + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unhandled machine: ${MACHINE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Failed to detect the build architecture! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + + set(build "[build_machine]\n") # Machine the build is performed on + string(APPEND build "endian = 'little'\n") + if(CMAKE_HOST_WIN32) + string(APPEND build "system = 'windows'\n") + elseif(CMAKE_HOST_APPLE) + string(APPEND build "system = 'darwin'\n") + elseif(CYGWIN) + string(APPEND build "system = 'cygwin'\n") + elseif(CMAKE_HOST_UNIX) + string(APPEND build "system = 'linux'\n") + else() + set(build_unknown TRUE) + endif() + + if(DEFINED build_cpu_fam) + string(APPEND build "cpu_family = '${build_cpu_fam}'\n") + endif() + if(DEFINED build_cpu) + string(APPEND build "cpu = '${build_cpu}'") + endif() + if(NOT build_unknown) + set(${build_system} "${build}" PARENT_SCOPE) + endif() + + set(host_unkown FALSE) + if(VCPKG_TARGET_ARCHITECTURE MATCHES "(amd|AMD|x|X)64") + set(host_cpu_fam x86_64) + set(host_cpu x86_64) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "(x|X)86") + set(host_cpu_fam x86) + set(host_cpu i686) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)64$") + set(host_cpu_fam aarch64) + set(host_cpu armv8) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)$") + set(host_cpu_fam arm) + set(host_cpu armv7hl) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "loongarch64") + set(host_cpu_fam loongarch64) + set(host_cpu loongarch64) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "wasm32") + set(host_cpu_fam wasm32) + set(host_cpu wasm32) + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unsupported target architecture ${VCPKG_TARGET_ARCHITECTURE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the host_machine entry!" ) + endif() + set(host_unkown TRUE) + endif() + + set(host "[host_machine]\n") # host=target in vcpkg. + string(APPEND host "endian = 'little'\n") + if(NOT VCPKG_CMAKE_SYSTEM_NAME OR VCPKG_TARGET_IS_MINGW OR VCPKG_TARGET_IS_UWP) + set(meson_system_name "windows") + else() + string(TOLOWER "${VCPKG_CMAKE_SYSTEM_NAME}" meson_system_name) + endif() + string(APPEND host "system = '${meson_system_name}'\n") + string(APPEND host "cpu_family = '${host_cpu_fam}'\n") + string(APPEND host "cpu = '${host_cpu}'") + if(NOT host_unkown) + set(${host_system} "${host}" PARENT_SCOPE) + endif() + + if(NOT build_cpu_fam MATCHES "${host_cpu_fam}" + OR VCPKG_TARGET_IS_ANDROID OR VCPKG_TARGET_IS_IOS OR VCPKG_TARGET_IS_UWP + OR (VCPKG_TARGET_IS_MINGW AND NOT CMAKE_HOST_WIN32)) + set(${is_cross} TRUE PARENT_SCOPE) + endif() +endfunction() + +function(z_vcpkg_meson_setup_extra_windows_variables config_type) + ## b_vscrt + if(VCPKG_CRT_LINKAGE STREQUAL "static") + set(crt_type "mt") + else() + set(crt_type "md") + endif() + if(config_type STREQUAL "DEBUG") + set(crt_type "${crt_type}d") + endif() + set(MESON_VSCRT_LINKAGE "b_vscrt = '${crt_type}'" PARENT_SCOPE) + ## winlibs + separate_arguments(c_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_C_STANDARD_LIBRARIES}") + separate_arguments(cpp_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_CXX_STANDARD_LIBRARIES}") + z_vcpkg_meson_convert_list_to_python_array(c_winlibs ${c_winlibs}) + z_vcpkg_meson_convert_list_to_python_array(cpp_winlibs ${cpp_winlibs}) + set(MESON_WINLIBS "c_winlibs = ${c_winlibs}\n") + string(APPEND MESON_WINLIBS "cpp_winlibs = ${cpp_winlibs}") + set(MESON_WINLIBS "${MESON_WINLIBS}" PARENT_SCOPE) +endfunction() + +function(z_vcpkg_meson_setup_variables config_type) + set(meson_var_list VSCRT_LINKAGE WINLIBS MT AR RC C C_LD CXX CXX_LD OBJC OBJC_LD OBJCXX OBJCXX_LD FC FC_LD WINDRES CFLAGS CXXFLAGS OBJCFLAGS OBJCXXFLAGS FCFLAGS SHARED_LINKER_FLAGS) + foreach(var IN LISTS meson_var_list) + set(MESON_${var} "") + endforeach() + + if(VCPKG_TARGET_IS_WINDOWS) + z_vcpkg_meson_setup_extra_windows_variables("${config_type}") + endif() + + z_vcpkg_meson_set_proglist_variables("${config_type}") + z_vcpkg_meson_set_flags_variables("${config_type}") + + foreach(var IN LISTS meson_var_list) + set(MESON_${var} "${MESON_${var}}" PARENT_SCOPE) + endforeach() +endfunction() + +function(vcpkg_generate_meson_cmd_args) + cmake_parse_arguments(PARSE_ARGV 0 arg + "" + "OUTPUT;CONFIG" + "OPTIONS;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_PROPERTIES" + ) + + if(NOT arg_LANGUAGES) + set(arg_LANGUAGES C CXX) + endif() + + vcpkg_list(JOIN arg_ADDITIONAL_BINARIES "\n" MESON_ADDITIONAL_BINARIES) + vcpkg_list(JOIN arg_ADDITIONAL_PROPERTIES "\n" MESON_ADDITIONAL_PROPERTIES) + + set(buildtype "${arg_CONFIG}") + + if(NOT VCPKG_CHAINLOAD_TOOLCHAIN_FILE) + z_vcpkg_select_default_vcpkg_chainload_toolchain() + endif() + vcpkg_list(APPEND VCPKG_CMAKE_CONFIGURE_OPTIONS "-DVCPKG_LANGUAGES=${arg_LANGUAGES}") + vcpkg_cmake_get_vars(cmake_vars_file) + debug_message("Including cmake vars from: ${cmake_vars_file}") + include("${cmake_vars_file}") + + vcpkg_list(APPEND arg_OPTIONS --backend ninja --wrap-mode nodownload -Doptimization=plain) + + z_vcpkg_get_build_and_host_system(MESON_HOST_MACHINE MESON_BUILD_MACHINE IS_CROSS) + + if(arg_CONFIG STREQUAL "DEBUG") + set(suffix "dbg") + else() + string(SUBSTRING "${arg_CONFIG}" 0 3 suffix) + string(TOLOWER "${suffix}" suffix) + endif() + set(meson_input_file_${buildtype} "${CURRENT_BUILDTREES_DIR}/meson-${TARGET_TRIPLET}-${suffix}.log") + + if(IS_CROSS) + # VCPKG_CROSSCOMPILING is not used since it regresses a lot of ports in x64-windows-x triplets + # For consistency this should proably be changed in the future? + vcpkg_list(APPEND arg_OPTIONS --native "${SCRIPTS}/buildsystems/meson/none.txt") + vcpkg_list(APPEND arg_OPTIONS --cross "${meson_input_file_${buildtype}}") + else() + vcpkg_list(APPEND arg_OPTIONS --native "${meson_input_file_${buildtype}}") + endif() + + # User provided cross/native files + if(VCPKG_MESON_NATIVE_FILE) + vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE}") + endif() + if(VCPKG_MESON_NATIVE_FILE_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE_${buildtype}}") + endif() + if(VCPKG_MESON_CROSS_FILE) + vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE}") + endif() + if(VCPKG_MESON_CROSS_FILE_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE_${buildtype}}") + endif() + + vcpkg_list(APPEND arg_OPTIONS --libdir lib) # else meson install into an architecture describing folder + vcpkg_list(APPEND arg_OPTIONS --pkgconfig.relocatable) + + if(arg_CONFIG STREQUAL "RELEASE") + vcpkg_list(APPEND arg_OPTIONS -Ddebug=false --prefix "${CURRENT_PACKAGES_DIR}") + vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") + if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}/share']") + else() + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug']") + endif() + elseif(arg_CONFIG STREQUAL "DEBUG") + vcpkg_list(APPEND arg_OPTIONS -Ddebug=true --prefix "${CURRENT_PACKAGES_DIR}/debug" --includedir ../include) + vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/debug/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") + if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/share']") + else() + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}']") + endif() + else() + message(FATAL_ERROR "Unknown configuration. Only DEBUG and RELEASE are valid values.") + endif() + + # Allow overrides / additional configuration variables from triplets + if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS) + vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS}) + endif() + if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}}) + endif() + + if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic") + set(MESON_DEFAULT_LIBRARY shared) + else() + set(MESON_DEFAULT_LIBRARY static) + endif() + set(MESON_CMAKE_BUILD_TYPE "${cmake_build_type_${buildtype}}") + z_vcpkg_meson_setup_variables(${buildtype}) + configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/meson.template.in" "${meson_input_file_${buildtype}}" @ONLY) + set("${arg_OUTPUT}" ${arg_OPTIONS} PARENT_SCOPE) +endfunction() + +function(vcpkg_configure_meson) + # parse parameters such that semicolons in options arguments to COMMAND don't get erased + cmake_parse_arguments(PARSE_ARGV 0 arg + "NO_PKG_CONFIG" + "SOURCE_PATH" + "OPTIONS;OPTIONS_DEBUG;OPTIONS_RELEASE;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_NATIVE_BINARIES;ADDITIONAL_CROSS_BINARIES;ADDITIONAL_PROPERTIES" + ) + + if(DEFINED arg_ADDITIONAL_NATIVE_BINARIES OR DEFINED arg_ADDITIONAL_CROSS_BINARIES) + message(WARNING "Options ADDITIONAL_(NATIVE|CROSS)_BINARIES have been deprecated. Only use ADDITIONAL_BINARIES!") + endif() + vcpkg_list(APPEND arg_ADDITIONAL_BINARIES ${arg_ADDITIONAL_NATIVE_BINARIES} ${arg_ADDITIONAL_CROSS_BINARIES}) + vcpkg_list(REMOVE_DUPLICATES arg_ADDITIONAL_BINARIES) + + file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") + file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") + + vcpkg_find_acquire_program(MESON) + + get_filename_component(CMAKE_PATH "${CMAKE_COMMAND}" DIRECTORY) + vcpkg_add_to_path("${CMAKE_PATH}") # Make CMake invokeable for Meson + + vcpkg_find_acquire_program(NINJA) + + if(NOT arg_NO_PKG_CONFIG) + vcpkg_find_acquire_program(PKGCONFIG) + set(ENV{PKG_CONFIG} "${PKGCONFIG}") + endif() + + vcpkg_find_acquire_program(PYTHON3) + get_filename_component(PYTHON3_DIR "${PYTHON3}" DIRECTORY) + vcpkg_add_to_path(PREPEND "${PYTHON3_DIR}") + + set(buildtypes "") + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + set(buildname "DEBUG") + set(cmake_build_type_${buildname} "Debug") + vcpkg_list(APPEND buildtypes "${buildname}") + set(path_suffix_${buildname} "debug/") + set(suffix_${buildname} "dbg") + endif() + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + set(buildname "RELEASE") + set(cmake_build_type_${buildname} "Release") + vcpkg_list(APPEND buildtypes "${buildname}") + set(path_suffix_${buildname} "") + set(suffix_${buildname} "rel") + endif() + + # configure build + foreach(buildtype IN LISTS buildtypes) + message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}}") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}") + + vcpkg_generate_meson_cmd_args( + OUTPUT cmd_args + CONFIG ${buildtype} + LANGUAGES ${arg_LANGUAGES} + OPTIONS ${arg_OPTIONS} ${arg_OPTIONS_${buildtype}} + ADDITIONAL_BINARIES ${arg_ADDITIONAL_BINARIES} + ADDITIONAL_PROPERTIES ${arg_ADDITIONAL_PROPERTIES} + ) + + vcpkg_execute_required_process( + COMMAND ${MESON} setup ${cmd_args} ${arg_SOURCE_PATH} + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}" + LOGNAME config-${TARGET_TRIPLET}-${suffix_${buildtype}} + SAVE_LOG_FILES + meson-logs/meson-log.txt + meson-info/intro-dependencies.json + meson-logs/install-log.txt + ) + + message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}} done") + endforeach() +endfunction() diff --git a/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake new file mode 100644 index 0000000000..0351f271a4 --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake @@ -0,0 +1,71 @@ +function(vcpkg_install_meson) + cmake_parse_arguments(PARSE_ARGV 0 arg "ADD_BIN_TO_PATH" "" "") + + vcpkg_find_acquire_program(NINJA) + unset(ENV{DESTDIR}) # installation directory was already specified with '--prefix' option + + if(VCPKG_TARGET_IS_OSX) + vcpkg_backup_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) + set(ENV{SDKROOT} "${VCPKG_DETECTED_CMAKE_OSX_SYSROOT}") + set(ENV{MACOSX_DEPLOYMENT_TARGET} "${VCPKG_DETECTED_CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + + foreach(buildtype IN ITEMS "debug" "release") + if(DEFINED VCPKG_BUILD_TYPE AND NOT VCPKG_BUILD_TYPE STREQUAL buildtype) + continue() + endif() + + if(buildtype STREQUAL "debug") + set(short_buildtype "dbg") + else() + set(short_buildtype "rel") + endif() + + message(STATUS "Package ${TARGET_TRIPLET}-${short_buildtype}") + if(arg_ADD_BIN_TO_PATH) + vcpkg_backup_env_variables(VARS PATH) + if(buildtype STREQUAL "debug") + vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/debug/bin") + else() + vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/bin") + endif() + endif() + vcpkg_execute_required_process( + COMMAND "${NINJA}" install -v + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${short_buildtype}" + LOGNAME package-${TARGET_TRIPLET}-${short_buildtype} + ) + if(arg_ADD_BIN_TO_PATH) + vcpkg_restore_env_variables(VARS PATH) + endif() + endforeach() + + vcpkg_list(SET renamed_libs) + if(VCPKG_TARGET_IS_WINDOWS AND VCPKG_LIBRARY_LINKAGE STREQUAL static AND NOT VCPKG_TARGET_IS_MINGW) + # Meson names all static libraries lib.a which basically breaks the world + file(GLOB_RECURSE gen_libraries "${CURRENT_PACKAGES_DIR}*/**/lib*.a") + foreach(gen_library IN LISTS gen_libraries) + get_filename_component(libdir "${gen_library}" DIRECTORY) + get_filename_component(libname "${gen_library}" NAME) + string(REGEX REPLACE ".a$" ".lib" fixed_librawname "${libname}") + string(REGEX REPLACE "^lib" "" fixed_librawname "${fixed_librawname}") + file(RENAME "${gen_library}" "${libdir}/${fixed_librawname}") + # For cmake fixes. + string(REGEX REPLACE ".a$" "" origin_librawname "${libname}") + string(REGEX REPLACE ".lib$" "" fixed_librawname "${fixed_librawname}") + vcpkg_list(APPEND renamed_libs ${fixed_librawname}) + set(${librawname}_old ${origin_librawname}) + set(${librawname}_new ${fixed_librawname}) + endforeach() + file(GLOB_RECURSE cmake_files "${CURRENT_PACKAGES_DIR}*/*.cmake") + foreach(cmake_file IN LISTS cmake_files) + foreach(current_lib IN LISTS renamed_libs) + vcpkg_replace_string("${cmake_file}" "${${current_lib}_old}" "${${current_lib}_new}" IGNORE_UNCHANGED) + endforeach() + endforeach() + endif() + + if(VCPKG_TARGET_IS_OSX) + vcpkg_restore_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) + endif() +endfunction() diff --git a/archived/projt-launcher/cmake/vcpkg-triplets/universal-osx.cmake b/archived/projt-launcher/cmake/vcpkg-triplets/universal-osx.cmake new file mode 100644 index 0000000000..1c91a5650e --- /dev/null +++ b/archived/projt-launcher/cmake/vcpkg-triplets/universal-osx.cmake @@ -0,0 +1,8 @@ +# See https://github.com/microsoft/vcpkg/discussions/19454 +# NOTE: Try to keep in sync with default arm64-osx definition +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) + +set(VCPKG_CMAKE_SYSTEM_NAME Darwin) +set(VCPKG_OSX_ARCHITECTURES "arm64;x86_64") diff --git a/archived/projt-launcher/default.nix b/archived/projt-launcher/default.nix new file mode 100644 index 0000000000..5ecef55905 --- /dev/null +++ b/archived/projt-launcher/default.nix @@ -0,0 +1,4 @@ +(import (fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; + sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; +}) { src = ./.; }).defaultNix diff --git a/archived/projt-launcher/docs/APPLE_SILICON_RATIONALE.md b/archived/projt-launcher/docs/APPLE_SILICON_RATIONALE.md new file mode 100644 index 0000000000..a192cc83c6 --- /dev/null +++ b/archived/projt-launcher/docs/APPLE_SILICON_RATIONALE.md @@ -0,0 +1,46 @@ +# Apple Silicon rationale and migration notes + +This document contains the background, technical rationale, and alternatives related to the project's decision to target Apple Silicon (ARM64) for official builds and to deprecate active Intel macOS testing. + +## Summary + +As of 0.0.3 the project targets Apple Silicon (ARM64) for official builds and day-to-day testing. Intel macOS (x86_64) compatibility still exists in some artifacts (for example older releases or universal `.app` bundles built via CMake), but active Intel-specific testing and certain Intel build pipelines (notably the Nix flake builds) are deprecated. + +This decision is driven by several practical factors affecting build reproducibility, CI availability, and long-term maintenance costs for a small and shrinking user base. It does not mean that universally packaged `.app` bundles cannot run on Intel macOS; it means we do not guarantee or actively test Intel-specific fixes across our primary build and CI workflows. + +## Reasons (technical) + +- Toolchain and platform support: modern macOS toolchains, SDKs, and some third-party libraries are increasingly ARM-first. Building and testing against the latest toolchains is simpler and faster on Apple Silicon. + +- Reproducible/infrastructure builds: our Nix flake-based builds and other reproducible build paths are significantly slower and more brittle on x86_64-darwin. Maintaining separate derivations, self-hosted runners, or extensive workarounds increases maintenance burden. + +- CI availability: many cloud CI providers now prioritize ARM macOS runners; securing reliable, up-to-date Intel macOS runners is costly or impractical. + +- Maintenance cost vs. user share: supporting Intel macOS requires ongoing CI/packaging complexity and platform-specific debugging. For a small and declining user share this reduces overall project velocity. + +## Observed issues in Intel builds (examples) + +During testing we observed higher incidence of platform-specific problems on Intel macOS, including but not limited to: + +- Increased crashes in Qt WebEngine-heavy paths +- Rendering differences or glitches that did not appear on Apple Silicon +- Increased resource usage and slower startup times in some scenarios + +These are presented as observations from our internal testing; specific projects or environments may have different results. + +## Alternatives for Intel Mac users + +If you are on an Intel Mac and need to run ProjT Launcher, options include: + +1. Use the last Intel-compatible release (for example v0.0.2). +2. Run a Linux x86_64 build in a VM or container. +3. Use a community-maintained launcher or fork that continues Intel support. + +## Notes for packagers and distributors + +- Our `.app` bundles built via CMake may be universal, but we do not actively test those on Intel hardware. +- If you package for a distribution that requires Intel macOS support you may need to maintain separate build pipelines and perform independent testing. + +## Contact + +If you represent a user population that requires continued Intel macOS support (for example a distribution or institutional environment), please open a GitHub issue describing the scope and constraints — we can discuss options (sponsored CI, community-maintained runners, or targeted backporting). diff --git a/archived/projt-launcher/docs/BUILD_SYSTEM.md b/archived/projt-launcher/docs/BUILD_SYSTEM.md new file mode 100644 index 0000000000..7003e272ab --- /dev/null +++ b/archived/projt-launcher/docs/BUILD_SYSTEM.md @@ -0,0 +1,996 @@ +# ProjT Launcher — Build System Documentation + +> **Version:** 0.0.5-1 +> **Build System:** GNU Make + Kconfig +> **Last Updated:** 2026-02-07 + +--- + +## Table of Contents + +1. [Architecture Overview](#1-architecture-overview) +2. [Quick Start](#2-quick-start) +3. [Directory Layout](#3-directory-layout) +4. [Makefile Include Graph](#4-makefile-include-graph) +5. [Build Phases](#5-build-phases) +6. [Configuration System (Kconfig)](#6-configuration-system-kconfig) +7. [Toolchain Selection](#7-toolchain-selection) +8. [mk/ File Reference](#8-mk-file-reference) +9. [Module System](#9-module-system) +10. [Qt Integration](#10-qt-integration) +11. [Test Infrastructure](#11-test-infrastructure) +12. [Packaging](#12-packaging) +13. [Cross-Compilation](#13-cross-compilation) +14. [CI/CD Integration](#14-cicd-integration) +15. [Make Targets Reference](#15-make-targets-reference) +16. [Variables Reference](#16-variables-reference) +17. [Troubleshooting](#17-troubleshooting) +18. [Design Decisions](#18-design-decisions) + +--- + +## 1. Architecture Overview + +The build system uses **GNU Make** as the build driver with **Kconfig** (the Linux kernel +configuration system) for build-time configuration. There is no CMake dependency for the +main build (CMake files exist only for Nix builds and cmark's internal build). + +### Key Design Principles + +- **Single entry point:** The top-level `Makefile` is the only user-facing interface +- **Kconfig for config:** All build options live in `Kconfig` and are set via `menuconfig`/`defconfig` +- **Modular mk/ files:** Build logic is split into focused `.mk` files under `mk/` +- **Per-module Makefiles:** Each library/component has its own `Makefile` invoked as a sub-make +- **Jobserver passthrough:** Parallel builds work across all sub-makes via GNU Make's jobserver +- **No recursive include hell:** Clear include chains with include guards where needed + +### Build Flow Summary + +``` +./configure → .config → make defconfig → syncconfig → make build + │ + ┌─────────┴─────────┠+ │ scripts/ │ + │ syncconfig.sh │ + │ │ + â–¼ â–¼ + auto.conf autoconf.h + (Make vars) (C #defines) + │ + ┌───────────────┼───────────────┠+ â–¼ â–¼ â–¼ + mk/targets.mk mk/tests.mk mk/package.mk + │ + ┌───────────┼───────────┬────────────┠+ â–¼ â–¼ â–¼ â–¼ + configure subtrees libs launcher + (.in→gen) (zlib,etc) (bzip2, (main app) + cmark,etc) +``` + +--- + +## 2. Quick Start + +### Linux (Native Build) + +```bash +# Install dependencies +sudo apt install build-essential qt6-base-dev qt6-5compat-dev \ + libssl-dev flex bison libncurses-dev + +# Configure +./configure # or: make defconfig +make menuconfig # optional: tweak settings + +# Build +make -j$(nproc) + +# Test +make test + +# Install +sudo make install PREFIX=/usr/local +``` + +### Windows (MSVC — from VS Developer Command Prompt) + +```cmd +configure.bat +make defconfig +make -j%NUMBER_OF_PROCESSORS% +``` + +### Windows (MinGW Cross-Compile from Linux) + +```bash +./configure --platform windows --cross-compile x86_64-w64-mingw32- +make defconfig && make -j$(nproc) +``` + +### macOS + +```bash +./configure +make defconfig && make -j$(sysctl -n hw.ncpu) +``` + +--- + +## 3. Directory Layout + +``` +ProjT-Launcher/ +├── Makefile # Top-level entry point (961 lines) +├── Kconfig # Configuration menu definitions +├── defconfig # Default configuration +├── configure # Unix configure script → generates .config +├── configure.bat # Windows configure script +├── scripts/ +│ └── syncconfig.sh # Generates auto.conf + autoconf.h from .config +│ +├── mk/ # Build system modules (16 active files) +│ ├── config.mk # Foundational: paths, auto.conf include, helpers +│ ├── host.mk # Host tool detection +│ ├── flags.mk # Compiler flag definitions +│ ├── toolchain.mk # Toolchain selection from Kconfig values +│ ├── rules.mk # Common compile/link pattern rules +│ ├── qt.mk # Qt framework integration +│ ├── qt-tools.mk # Qt tool paths (MOC, UIC, RCC) +│ ├── module.mk # Shared build rules for simple modules +│ ├── configure.mk # Template .in file processing +│ ├── targets.mk # Build orchestration (6 phases) +│ ├── subtrees.mk # Subtree library wrappers (zlib, tomlplusplus) +│ ├── tests.mk # Test build & execution +│ ├── package.mk # Packaging (DEB, RPM, AppImage, DMG, NSIS) +│ ├── platform.mk # Platform detection utilities +│ ├── toolchain-gcc.mk # Standalone GCC build +│ ├── toolchain-llvm.mk # Standalone LLVM/Clang build +│ └── unused/ # Dead code (preserved for reference) +│ ├── bootstrap.mk # Self-hosted toolchain bootstrap (unused) +│ ├── modules.mk # Central module definitions (unused) +│ ├── rules-full.mk # Extended build rules (unused) +│ ├── toolchain-full.mk # Advanced toolchain detection (unused) +│ ├── buildconfig.mk # Python-based config gen (unused) +│ ├── qt-gen.mk # Qt generation rules (unused) +│ ├── vars.mk # Version/branding vars (unused) +│ └── java.mk # Java build rules (unused) +│ +├── kconfig/ # Kconfig source (conf, mconf, nconf, qconf, gconf) +├── build/ # Build output directory (default O=build) +│ ├── .config # Active configuration +│ ├── include/ +│ │ ├── config/auto.conf # Make-includable config variables +│ │ └── generated/autoconf.h # C-includable config defines +│ ├── obj/ # Object files per module +│ ├── lib/ # Static/shared libraries +│ ├── bin/ # Final executables +│ ├── jars/ # Java JARs +│ └── tests/ # Test executables +│ +├── launcher/ # Main launcher source (993-line Makefile) +│ ├── Makefile +│ ├── console/ +│ ├── icons/ +│ ├── java/ +│ ├── launch/ +│ ├── logs/ +│ ├── meta/ +│ ├── minecraft/ +│ ├── modplatform/ +│ ├── net/ +│ ├── news/ +│ ├── screenshots/ +│ ├── settings/ +│ ├── tasks/ +│ ├── tools/ +│ ├── translations/ +│ ├── ui/ +│ └── updater/ +│ +├── bzip2/ # libbz2 (simple module) +├── murmur2/ # MurmurHash2 (simple module) +├── rainbow/ # Color library (Qt dep, simple module) +├── qdcss/ # Qt CSS parser (Qt dep, simple module) +├── systeminfo/ # System info (simple module) +├── libnbtplusplus/ # NBT parser (simple module) +├── gamemode/ # Linux GameMode (header-only) +├── libpng/ # PNG library (custom Makefile) +├── libqrencode/ # QR code library (custom Makefile) +├── cmark/ # CommonMark parser (uses internal CMake) +├── quazip/ # Qt ZIP library (Qt MOC module) +├── LocalPeer/ # IPC library (Qt MOC module) +├── buildconfig/ # Generated BuildConfig.cpp +├── javacheck/ # Java version checker +├── launcherjava/ # Java launcher wrappers +├── zlib/ # zlib source (built by subtrees.mk) +├── tomlplusplus/ # TOML parser (header-only) +├── json/ # nlohmann JSON (header-only) +├── tests/ # Test sources +└── fuzz/ # Fuzzing test sources +``` + +--- + +## 4. Makefile Include Graph + +### Main Makefile (entry point) + +``` +Makefile +├── include kconfig/Makefile (Kconfig tool build rules) +├── -include auto.conf (CONFIG_* variables) +├── $(MAKE) -f mk/targets.mk (build, libs, launcher, etc.) +├── $(MAKE) -f mk/tests.mk (test, tests-build) +├── $(MAKE) -f mk/package.mk (package-deb, package-rpm, etc.) +├── $(MAKE) -f mk/toolchain-gcc.mk (toolchain-gcc) +└── $(MAKE) -f mk/toolchain-llvm.mk (toolchain-llvm) +``` + +### mk/ Include Dependencies + +``` +config.mk ↠foundational, included by 8+ files +├── host.mk ↠included by config.mk (indirectly via toolchain.mk) +├── toolchain.mk ↠includes config.mk + host.mk +│ └── flags.mk ↠includes toolchain.mk +│ └── qt-tools.mk ↠includes flags.mk +│ └── rules.mk ↠includes qt-tools.mk +│ └── module.mk ↠includes rules.mk (used by ~27 modules) +├── configure.mk ↠includes config.mk +├── qt.mk ↠includes config.mk +├── subtrees.mk ↠includes config.mk +├── tests.mk ↠standalone (receives vars via export) +├── package.mk ↠includes config.mk + platform.mk +├── platform.mk ↠includes config.mk +├── toolchain-gcc.mk ↠standalone +└── toolchain-llvm.mk ↠standalone +``` + +### Module Makefile Pattern + +All simple modules follow this pattern: +```makefile +# In bzip2/Makefile, murmur2/Makefile, etc. +lib := bz2 # Library name → libXXX.a +lib-y := blocksort.c ... # Source files +includes-y := bzip2 # Include paths +ccflags-y := -DFOO # Extra C flags (optional) +qt-modules-y := Core # Qt modules needed (optional) +include $(srctree)/mk/module.mk +``` + +--- + +## 5. Build Phases + +The build executes in 6 sequential phases (defined in `mk/targets.mk`): + +### Phase 1: Configure +- Processes `.in` template files → generates C/C++ source files +- `BuildConfig.cpp.in` → `build/obj/generated/BuildConfig.cpp` +- `Launcher.in` → `build/obj/bin/LauncherScript` +- `pnglibconf.h.prebuilt` → `build/obj/generated/include/pnglibconf.h` +- `nbt_export.h`, `cmark_config.h`, `cmark_export.h`, `cmark_version.h` + +### Phase 2: Subtrees +- Builds vendored libraries from source subtrees +- **zlib:** Compiled from `zlib/` → `build/lib/libz.a` +- **tomlplusplus:** Header-only, stamp file only +- **json:** Header-only, stamp file only +- **Qt (if bundled):** Built from `qt/` subtree + +### Phase 3: Qt Build +- If `CONFIG_QT_BUNDLED=y`: builds Qt from source +- If system Qt: skipped (uses pkg-config) + +### Phase 4: Libraries (3 tiers) +- **Tier 0:** No dependencies → bzip2, murmur2 +- **Tier 1:** zlib dependencies → (currently empty, reserved) +- **Tier 2:** General deps → libpng, cmark, libnbtplusplus, libqrencode +- **Tier 3:** Qt dependencies → rainbow, qdcss, LocalPeer, quazip + +### Phase 5: Java +- **javacheck:** Java version detection utility +- **launcherjava:** Java launcher wrappers (ProjTLaunch.jar, ProjTLaunchLegacy.jar) + +### Phase 6: Launcher +- Builds all launcher submodules (console, icons, java, launch, etc.) +- Links everything into `build/bin/projtlauncher` +- Uses `--whole-archive` to ensure all symbols are included + +--- + +## 6. Configuration System (Kconfig) + +### How It Works + +1. **`Kconfig`** (root file) defines all configuration options with types, defaults, help text +2. **`./configure`** (or `configure.bat`) creates initial `.config` in the build directory +3. **`make menuconfig`** opens the interactive ncurses-based configuration editor +4. **`scripts/syncconfig.sh`** converts `.config` → `auto.conf` (Make) + `autoconf.h` (C) + +### Configuration Files + +| File | Format | Purpose | +|------|--------|---------| +| `Kconfig` | Kconfig DSL | Option definitions | +| `defconfig` | key=value | Default values | +| `build/.config` | key=value | Active configuration | +| `build/include/config/auto.conf` | Make include | `CONFIG_*` variables for Make | +| `build/include/generated/autoconf.h` | C header | `#define CONFIG_*` for C/C++ | + +### Key Configuration Variables + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `CONFIG_CC` | string | `"gcc"` | C compiler | +| `CONFIG_CXX` | string | `"g++"` | C++ compiler | +| `CONFIG_BUILD_TYPE` | string | `"Debug"` | Build type (Debug/Release/RelWithDebInfo) | +| `CONFIG_BUILD_TESTS` | bool | `y` | Enable test building | +| `CONFIG_BUILD_FUZZERS` | bool | `n` | Enable fuzzer building | +| `CONFIG_ENABLE_LTO` | bool | `n` | Link-time optimization | +| `CONFIG_QT_VERSION_MAJOR` | string | `"6"` | Qt major version | +| `CONFIG_QT_PREFIX` | string | `""` | Qt installation prefix | +| `CONFIG_USE_BUNDLED_QT` | bool | `n` | Build Qt from source | +| `CONFIG_TARGET_LINUX` | bool | auto | Target Linux | +| `CONFIG_TARGET_WINDOWS` | bool | auto | Target Windows | +| `CONFIG_TARGET_MACOS` | bool | auto | Target macOS | + +### syncconfig.sh + +The `scripts/syncconfig.sh` script is a lightweight replacement for the kernel's `syncconfig` +mechanism. It: + +1. Reads `build/.config` +2. Extracts `CONFIG_*=value` lines +3. Generates `auto.conf` (for Make inclusion) +4. Generates `autoconf.h` (with `#define` for C/C++) +5. Uses `LC_ALL=C` to avoid locale-dependent regex issues + +**Critical:** The `LC_ALL=C` export at the top of `syncconfig.sh` is essential. Without it, +Turkish locale (`tr_TR`) breaks `[A-Za-z]` regex patterns, causing CONFIG variables with +uppercase `I` to be silently dropped. + +--- + +## 7. Toolchain Selection + +### Selection Priority (highest to lowest) + +1. **Command line:** `make CC=clang CXX=clang++` +2. **Kconfig config:** `CONFIG_CC="clang"` in `.config` +3. **Platform default:** Set in main Makefile per platform +4. **ccache/sccache wrapping:** Applied automatically if available + +### Platform Defaults + +| Platform | CC | CXX | LD | +|----------|----|----|-----| +| Linux | `gcc` | `g++` | `g++` | +| macOS | `clang` | `clang++` | `clang++` | +| Windows (MSVC) | `cl.exe` | `cl.exe` | `link.exe` | +| Windows (MinGW) | `$(CROSS_COMPILE)gcc` | `$(CROSS_COMPILE)g++` | `$(CROSS_COMPILE)g++` | + +### Kconfig Override Flow + +``` +Makefile: CC = gcc (platform default) + ↓ +-include auto.conf (loads CONFIG_CC="clang") + ↓ +cfg-unquote strips quotes (_CC_CFG = clang) + ↓ +CC := clang (override applied) + ↓ +CC := ccache clang (ccache wrapper, if available) +``` + +### ccache/sccache + +Detected automatically. Disable with `NO_CCACHE=1`: +```bash +make NO_CCACHE=1 build +``` + +--- + +## 8. mk/ File Reference + +### Active Files (16) + +#### `mk/config.mk` — Foundational Configuration +- **Lines:** 41 +- **Included by:** 8+ other mk/ files +- **Purpose:** Defines `OBJDIR`, `LIBDIR`, `BINDIR`, includes `auto.conf` and `.config`, + provides `cfg-yes`, `cfg-on`, `cfg-unquote` helper functions +- **Exports:** `CC`, `CXX`, `AR`, `RANLIB`, `OBJDIR`, `LIBDIR`, `BINDIR` + +#### `mk/host.mk` — Host Tool Detection +- **Lines:** 10 +- **Purpose:** Detects host tools (`HOSTCC`, `INSTALL`, `MKDIR`, etc.) + +#### `mk/toolchain.mk` — Toolchain Selection +- **Lines:** 46 +- **Includes:** `config.mk`, `host.mk` +- **Purpose:** Reads `CONFIG_CC`, `CONFIG_CXX`, `CONFIG_CROSS_COMPILE`, `CONFIG_SYSROOT` + from Kconfig and overrides `CC`/`CXX`/`AR`/`STRIP`/etc. +- **Also applies:** `CONFIG_CFLAGS_EXTRA`, `CONFIG_CXXFLAGS_EXTRA`, `CONFIG_LDFLAGS_EXTRA` + +#### `mk/flags.mk` — Compiler Flags +- **Lines:** 19 +- **Includes:** `toolchain.mk` +- **Purpose:** Sets up `CFLAGS`, `CXXFLAGS`, `LDFLAGS` with platform-appropriate values + +#### `mk/qt-tools.mk` — Qt Tool Paths +- **Lines:** 79 +- **Includes:** `flags.mk` +- **Purpose:** Locates `MOC`, `UIC`, `RCC`, `LRELEASE` tools via pkg-config or + `CONFIG_QT_HOST_BINS` prefix + +#### `mk/rules.mk` — Common Build Rules +- **Lines:** 63 +- **Includes:** `qt-tools.mk` +- **Purpose:** Defines `src-to-obj`, `build-static-lib`, `build-exe` macros and common + pattern rules for `.c` → `.o`, `.cpp` → `.o` compilation + +#### `mk/module.mk` — Module Build Template +- **Lines:** 151 +- **Included by:** ~27 module Makefiles +- **Purpose:** Standard build rules for simple modules. Expects `lib`, `lib-y`, `includes-y` + variables from the including Makefile. Handles MSVC vs GCC differences. + +#### `mk/configure.mk` — Template Processing +- **Lines:** 352 +- **Includes:** `config.mk` +- **Include guard:** `_MK_CONFIGURE_INCLUDED` +- **Purpose:** Processes `.in` template files with `@VAR@` → value substitutions. + Generates `BuildConfig.cpp`, `LauncherScript`, `pnglibconf.h`, `nbt_export.h`, + `cmark_config.h`, `cmark_export.h`, `cmark_version.h` +- **Key macro:** `SED_SUBST` — massive sed expression with all substitution variables + +#### `mk/qt.mk` — Qt Framework Integration +- **Lines:** 487 +- **Includes:** `config.mk` +- **Include guard:** `_MK_QT_INCLUDED` +- **Purpose:** Detects system Qt or builds bundled Qt. Provides `QT_MODULE_template` macro, + MOC/UIC/RCC rule templates, Qt CFLAGS/LIBS setup +- **Exports:** `QT_CFLAGS`, `QT_LIBS`, `QT_PREFIX`, `QT_HOST_BINS`, `MOC`, `UIC`, `RCC` + +#### `mk/targets.mk` — Build Orchestration +- **Lines:** 521 +- **Includes:** `configure.mk`, `qt.mk` +- **Purpose:** Defines the 6-phase build sequence. Contains `build_local` and `clean_local` + macros for consistent module building. Handles launcher linking with `--whole-archive`. +- **Key targets:** `build`, `configure`, `subtrees`, `qt-build`, `libs`, `launcher-all`, + `java-modules`, `tests`, `list-modules` + +#### `mk/subtrees.mk` — Subtree Library Wrappers +- **Lines:** 318 +- **Includes:** `config.mk` +- **Purpose:** Build wrappers for vendored subtree libraries: + - **zlib:** Full C build → `libz.a` + `libprojtZ.so` (renamed shared lib for tests) + - **tomlplusplus:** Header-only (stamp file) + - **json:** Header-only (stamp file) + - **Qt (bundled):** Conditional Qt build from `qt/` subtree + +#### `mk/tests.mk` — Test Infrastructure +- **Lines:** 312 +- **Standalone:** Invoked via `$(MAKE) -f mk/tests.mk` +- **Purpose:** Builds and runs 23 test executables. Links against all launcher libraries + with `--start-group`/`--end-group`. Uses `libprojtZ.so` (renamed shared zlib) to avoid + symbol conflicts with system Qt's zlib. +- **Key targets:** `test`, `check`, `tests-build`, `tests-run`, `tests-clean` + +#### `mk/package.mk` — Packaging +- **Lines:** 508 +- **Includes:** `config.mk`, `platform.mk` +- **Purpose:** Creates distribution packages: + - **DEB:** Debian/Ubuntu `.deb` packages + - **RPM:** Fedora/RHEL `.rpm` packages + - **AppImage:** Linux portable AppImage + - **DMG:** macOS disk image + - **NSIS:** Windows installer + - **TAR:** Source/binary tarballs + +#### `mk/platform.mk` — Platform Detection +- **Lines:** 279 +- **Includes:** `config.mk` +- **Purpose:** Comprehensive OS/architecture detection. Used by `package.mk` and + toolchain files. + +#### `mk/toolchain-gcc.mk` — GCC Build +- **Lines:** 181 +- **Standalone:** Invoked via `$(MAKE) -f mk/toolchain-gcc.mk` +- **Purpose:** Downloads and builds GCC from source or subtree for self-hosted builds + +#### `mk/toolchain-llvm.mk` — LLVM Build +- **Lines:** 189 +- **Standalone:** Invoked via `$(MAKE) -f mk/toolchain-llvm.mk` +- **Purpose:** Downloads and builds LLVM/Clang from source or subtree + +### Unused Files (8, in `mk/unused/`) + +These files were developed for planned features but are not referenced by any active +build path. Preserved for reference: + +| File | Reason Unused | +|------|---------------| +| `bootstrap.mk` | Self-hosted bootstrap — never integrated | +| `modules.mk` | Central module definitions — modules use per-directory Makefiles instead | +| `rules-full.mk` | Extended build rules (MSVC, Obj-C, shared libs) — not needed | +| `toolchain-full.mk` | Advanced toolchain auto-detection — simplified version used instead | +| `buildconfig.mk` | Python-based config generation — configure.mk handles this | +| `qt-gen.mk` | Qt generation rules — qt.mk handles this | +| `vars.mk` | Version/branding variables — main Makefile defines these | +| `java.mk` | Java build rules — targets.mk handles Java directly | + +--- + +## 9. Module System + +### Module Types + +#### Type 1: Simple Module (uses `mk/module.mk`) + +Modules that compile C/C++ sources into a static library: + +```makefile +# bzip2/Makefile +lib := bz2 +lib-y := blocksort.c huffman.c crctable.c randtable.c \ + compress.c decompress.c bzlib.c +includes-y := bzip2 +include $(srctree)/mk/module.mk +``` + +**Variables accepted by module.mk:** +| Variable | Required | Description | +|----------|----------|-------------| +| `lib` | Yes | Library name (output: `lib$(lib).a`) | +| `lib-y` | Yes | Source files (`.c` and/or `.cpp`) | +| `includes-y` | No | Include directories (relative to `srctree`) | +| `ccflags-y` | No | Extra C flags | +| `cxxflags-y` | No | Extra C++ flags | +| `qt-modules-y` | No | Qt modules needed (e.g., `Core Gui Widgets`) | + +**Modules using this pattern:** bzip2, murmur2, rainbow, qdcss, systeminfo, +libnbtplusplus, gamemode + +#### Type 2: Qt MOC Module + +Modules that need Qt's Meta-Object Compiler: + +```makefile +# LocalPeer/Makefile +lib := localpeer +lib-y := LocalPeer.cpp LockedFile.cpp LockedFile_unix.cpp +MOC_HEADERS := include/LocalPeer.h +EXTRA_OBJS := $(MOC_OBJS) +CUSTOM_STATIC_LIB_RULE := 1 +includes-y := LocalPeer/include LocalPeer +qt-modules-y := Core Network +include $(srctree)/mk/module.mk +``` + +**Modules using this pattern:** quazip, LocalPeer + +#### Type 3: Custom Makefile + +Modules with complex build logic: + +- **libpng:** Standalone C build with custom flags +- **libqrencode:** Standalone C build with own CFLAGS +- **buildconfig:** Compiles generated `BuildConfig.cpp` +- **cmark:** Uses its **own internal CMake** build system +- **launcher/:** Complex build with MOC, RCC, multiple submodules + +#### Type 4: Java Module + +```makefile +# javacheck/Makefile — compiles .java → .jar +``` + +--- + +## 10. Qt Integration + +### System Qt (Default) + +Qt is detected via `pkg-config` or the `CONFIG_QT_PREFIX` path: + +```bash +# Auto-detect via pkg-config +./configure + +# Specify prefix manually +./configure --qt-prefix /opt/qt6 +``` + +### Bundled Qt + +When `CONFIG_USE_BUNDLED_QT=y`, Qt is built from the `qt/` subtree: + +```bash +make menuconfig # Enable "Use Bundled Qt" +make qt-build # Build Qt from source +make build # Build the project +``` + +### Qt Tool Discovery + +The `mk/qt-tools.mk` file locates Qt tools in this order: + +1. `CONFIG_QT_HOST_BINS` (explicit path from Kconfig) +2. pkg-config `Qt6Core` `host_bins` variable +3. Common system paths (`/usr/lib/qt6/libexec/`, etc.) + +### Qt Modules Used + +| Qt Module | Used By | +|-----------|---------| +| Core | All Qt-dependent modules | +| Gui | launcher/ui | +| Widgets | launcher/ui | +| Network | launcher/net, LocalPeer | +| Xml | launcher/meta | +| Concurrent | launcher/tasks | +| Core5Compat | Various | + +--- + +## 11. Test Infrastructure + +### Overview + +- **23 test executables**, each testing a specific component +- Built by `mk/tests.mk`, run sequentially +- Each test links against ALL launcher libraries (to resolve dependencies) +- Uses `libprojtZ.so` (renamed shared zlib) to avoid symbol conflicts with system Qt + +### Why libprojtZ.so? + +System Qt links against system zlib. Our static `libz.a` has identical symbols, causing +`Z_VERSION_ERROR` at runtime. Solution: build zlib as a shared library with a unique name +(`libprojtZ.so.1`) and link tests against it instead. + +### Running Tests + +```bash +make test # Build + run all tests +make check # Same as test +make tests-build # Build only (no run) +make TEST_VERBOSE=1 test # Verbose output +make TEST_FILTER=Version test # Run only matching tests +``` + +### Test List + +| Test | Component | +|------|-----------| +| `Version_test` | Version parsing | +| `GradleSpecifier_test` | Gradle dependency specifiers | +| `RuntimeVersion_test` | Java runtime version parsing | +| `GZip_test` | GZip compression | +| `ParseUtils_test` | General parsing utilities | +| `JavaVersion_test` | Java version detection | +| `INIFile_test` | INI file parsing | +| `Library_test` | Library management | +| `MojangVersionFormat_test` | Mojang version format | +| `Packwiz_test` | Packwiz modpack format | +| `Index_test` | Index management | +| `MetaComponentParse_test` | Meta component parsing | +| `DataPackParse_test` | Data pack parsing | +| `ResourcePackParse_test` | Resource pack parsing | +| `TexturePackParse_test` | Texture pack parsing | +| `ShaderPackParse_test` | Shader pack parsing | +| `WorldSaveParse_test` | World save parsing | +| `ResourceFolderModel_test` | Resource folder model | +| `Task_test` | Task system | +| `CatPack_test` | Cat pack handling | +| `XmlLogs_test` | XML log parsing | +| `LogEventParser_test` | Log event parsing | +| `ProjTExternalUpdater_test` | External updater (non-macOS only) | + +--- + +## 12. Packaging + +Packaging is handled by `mk/package.mk`. All package targets are invoked via: + +```bash +make package-deb # Debian/Ubuntu package +make package-rpm # RPM package +make package-appimage # Linux AppImage +make package-dmg # macOS disk image +make package-nsis # Windows NSIS installer +make package # Build all configured packages +``` + +### Package Output + +All packages are created in `build/packages/`. + +--- + +## 13. Cross-Compilation + +### Linux → Windows (MinGW) + +```bash +./configure --platform windows --cross-compile x86_64-w64-mingw32- +make defconfig && make -j$(nproc) +``` + +### Linux → macOS (requires osxcross) + +```bash +./configure --platform macos --cross-compile x86_64-apple-darwin- +make defconfig && make -j$(nproc) +``` + +### Linux x86_64 → Linux aarch64 + +```bash +./configure --arch aarch64 --cross-compile aarch64-linux-gnu- +make defconfig && make -j$(nproc) +``` + +### Key Variables + +| Variable | Example | Description | +|----------|---------|-------------| +| `TARGET_PLATFORM` | `linux`, `windows`, `macos` | Target OS | +| `TARGET_ARCH` | `x86_64`, `aarch64`, `i686` | Target architecture | +| `CROSS_COMPILE` | `x86_64-w64-mingw32-` | Toolchain prefix | +| `WINDOWS_TOOLCHAIN` | `mingw`, `msvc` | Windows toolchain choice | + +--- + +## 14. CI/CD Integration + +### CI Targets + +```bash +make ci-prepare # Prepare CI environment +make ci-lint # Run linting checks +make ci-release # Build + create release artifacts +``` + +### GitHub Actions Matrix + +The CI runs ~20 build configurations: +- Linux x86_64 (GCC, Clang) +- Linux aarch64 (cross-compile) +- Windows MSVC (x86_64) +- Windows MinGW (x86_64) +- macOS 14, 15, 26 (arm64) + +--- + +## 15. Make Targets Reference + +### Configuration + +| Target | Description | +|--------|-------------| +| `defconfig` | Load default configuration | +| `menuconfig` | Interactive ncurses configuration | +| `nconfig` | Alternative ncurses configuration | +| `xconfig` | Qt-based graphical configuration | +| `gconfig` | GTK-based graphical configuration | +| `oldconfig` | Update config, prompt for new options | +| `olddefconfig` | Update config, use defaults for new options | +| `savedefconfig` | Save minimal config to `defconfig` | +| `listnewconfig` | List new, unconfigured options | + +### Build + +| Target | Description | +|--------|-------------| +| `all` (default) | Build everything | +| `build` | Build the project | +| `configure` | Generate headers from `.in` files | +| `subtrees` | Build subtree dependencies | +| `qt-build` | Build Qt (if bundled) | +| `libs` | Build all libraries | +| `launcher-all` | Build launcher and submodules | +| `java-modules` | Build Java modules | + +### Install + +| Target | Description | +|--------|-------------| +| `install` | Install to `PREFIX` (default: `/usr/local`) | +| `install-bin` | Install binary only | +| `install-data` | Install data files only | +| `uninstall` | Remove installed files | + +### Testing + +| Target | Description | +|--------|-------------| +| `test` / `check` | Build and run all tests | +| `tests-build` | Build tests without running | +| `fuzz` | Run fuzzers (requires `CONFIG_BUILD_FUZZERS=y`) | + +### Packaging + +| Target | Description | +|--------|-------------| +| `package` | Build all configured packages | +| `package-deb` | Build `.deb` package | +| `package-rpm` | Build `.rpm` package | +| `package-appimage` | Build AppImage | +| `package-dmg` | Build macOS DMG | +| `package-nsis` | Build Windows NSIS installer | + +### Toolchain + +| Target | Description | +|--------|-------------| +| `toolchain-gcc` | Build GCC from source | +| `toolchain-llvm` | Build LLVM/Clang from source | +| `toolchain-all` | Build both | +| `toolchain-clean` | Clean toolchain builds | +| `toolchain-distclean` | Remove all toolchain files | +| `toolchain-help` | Show toolchain options | + +### Utility + +| Target | Description | +|--------|-------------| +| `info` | Show project and build information | +| `version` | Show version string | +| `listconfig` | Show configuration summary | +| `list-modules` | List all available modules | +| `clean` | Remove build artifacts (obj, lib, bin) | +| `distclean` | Remove everything including `.config` | +| `mrproper` | Alias for `distclean` | +| `help` | Show help text | + +--- + +## 16. Variables Reference + +### User-Settable Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `V` | `0` | Verbose build (`V=1` for full commands) | +| `O` | `build` | Output directory | +| `PREFIX` | `/usr/local` | Installation prefix | +| `DESTDIR` | (empty) | Staging directory for packages | +| `JOBS` | `$(nproc)` | Parallel job count | +| `NO_CCACHE` | (unset) | Set to `1` to disable ccache | +| `CC` | (auto) | C compiler | +| `CXX` | (auto) | C++ compiler | +| `CROSS_COMPILE` | (empty) | Toolchain prefix | +| `TARGET_PLATFORM` | (auto) | Target OS | +| `TARGET_ARCH` | (auto) | Target architecture | +| `WINDOWS_TOOLCHAIN` | `mingw` | Windows toolchain (`mingw`/`msvc`) | + +### Internal Variables (read-only) + +| Variable | Description | +|----------|-------------| +| `srctree` | Source tree root (always `$(CURDIR)`) | +| `KBUILD_OUTPUT` | Absolute path to build output | +| `KCONFIG_CONFIG` | Path to `.config` file | +| `KCONFIG_AUTOCONFIG` | Path to `auto.conf` | +| `HOST_PLATFORM` | Host OS (`linux`/`macos`/`windows`) | +| `HOST_ARCH` | Host architecture | +| `OBJDIR` | Object file directory (`build/obj`) | +| `LIBDIR` | Library directory (`build/lib`) | +| `BINDIR` | Binary directory (`build/bin`) | + +--- + +## 17. Troubleshooting + +### Build fails with "CONFIG not found" or missing CONFIG variables + +**Cause:** Locale-dependent regex in `syncconfig.sh`. +**Fix:** Ensure `LC_ALL=C` is set in `scripts/syncconfig.sh` (already applied). + +```bash +# Verify: should show 70 CONFIG entries +grep -c '^CONFIG_' build/include/config/auto.conf +``` + +### CC/CXX empty in `make info` + +**Cause:** `MAKEFLAGS += -r -R` disables built-in variables, breaking `CC ?=` assignments. +**Fix:** Main Makefile now uses unconditional `CC =` and applies CONFIG_CC override after +auto.conf include (already fixed). + +### Tests fail with "libprojtZ not found" + +**Cause:** The shared zlib (`libprojtZ.so`) wasn't being built before test linking. +**Fix:** `mk/tests.mk` now includes `zlib-shared` as a prerequisite of `tests-build` +(already fixed). + +### "Tests not enabled" even though BUILD_TESTS=y in .config + +**Cause:** syncconfig.sh failed to generate `auto.conf` correctly (locale bug). +**Fix:** See "CONFIG not found" above. + +### ccache wrapping an empty CC + +**Cause:** ccache section ran before CONFIG_CC override. +**Fix:** ccache/sccache wrapping now happens after auto.conf values are applied. + +### Out-of-tree build + +```bash +make O=../build-release defconfig +make O=../build-release -j$(nproc) +``` + +### Windows: "make is not recognized" + +Install make via: +- **MSYS2:** `pacman -S make` +- **Chocolatey:** `choco install make` +- **Visual Studio:** Use `nmake` or install GNU Make + +### Nix builds + +Nix builds use `CMakeLists.txt` (not the Make build system). The CMake files are +maintained separately for Nix compatibility. + +--- + +## 18. Design Decisions + +### Why Make instead of CMake? + +1. **Simpler:** 961-line Makefile + 16 mk/ files vs. 50+ CMakeLists.txt files +2. **Faster:** No generation step, direct compilation +3. **Kconfig:** Native integration with Linux kernel's proven configuration system +4. **Transparent:** Easy to debug with `make V=1` and `make -p` +5. **Cross-platform:** Works on Linux, macOS, Windows (MSVC, MinGW, MSYS2) + +### Why Kconfig? + +1. **Battle-tested:** Used by the Linux kernel for 20+ years +2. **Dependency tracking:** Options can depend on other options +3. **Multiple UIs:** `menuconfig` (ncurses), `xconfig` (Qt), `gconfig` (GTK) +4. **Minimal footprint:** Pure C, builds as part of the project + +### Why libprojtZ.so for tests? + +System Qt links against system zlib. Our static `libz.a` has the same symbols. +When both are linked into the same binary, the zlib version check fails +(`Z_VERSION_ERROR`). Using a renamed shared library (`libprojtZ.so`) with `-rpath` +avoids this conflict. + +### Why `-r -R` in MAKEFLAGS? + +`-r` disables built-in implicit rules (like `.c.o:`) and `-R` disables built-in +variable definitions (like `CC=cc`). This eliminates ~150 implicit rules and variables +that would otherwise slow down Make's pattern matching. The speed improvement is +significant for large projects. + +**Trade-off:** All variables must be explicitly set (no fallback to `CC=cc`). +The Makefile uses unconditional `CC = gcc` (not `CC ?= gcc`) to handle this. + +### Why `SHELL := /bin/bash`? + +The build system uses bash-specific features: +- `PIPESTATUS` array for checking exit codes in pipelines +- `set -o pipefail` for proper error propagation +- Process substitution `<()` in some scripts + +--- + +## Appendix: File Statistics + +| Component | Files | Lines | +|-----------|-------|-------| +| Main Makefile | 1 | 977 | +| Active mk/ files | 16 | ~3,200 | +| Module Makefiles | ~27 | ~2,500 | +| Launcher Makefile | 1 | 993 | +| Launcher sub-Makefiles | 18 | ~3,000 | +| Total build system | ~63 | ~10,700 | diff --git a/archived/projt-launcher/docs/FUZZING.md b/archived/projt-launcher/docs/FUZZING.md new file mode 100644 index 0000000000..4f6ffe8fa9 --- /dev/null +++ b/archived/projt-launcher/docs/FUZZING.md @@ -0,0 +1,39 @@ +# Fuzzing + +This project ships libFuzzer targets for key parsers and data paths. + +## Targets + +- `fuzz_nbt_reader`: NBT stream reader (libnbt++) +- `fuzz_qjson_parse`: Qt JSON parser +- `fuzz_gzip`: GZip inflate/deflate helpers +- `fuzz/meta/fuzz_meta_models.py`: Python Atheris targets for metadata models +- `bot/fuzz/fastcheck.test.js`: JS property fuzzing for bot helpers + +## Local build (Linux/macOS) + +```bash +cmake -S . -B build-fuzz -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_TESTING=OFF \ + -DBUILD_FUZZERS=ON \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ + +cmake --build build-fuzz --target fuzz_nbt_reader fuzz_qjson_parse fuzz_gzip +``` + +Run a fuzzer: + +```bash +./build-fuzz/fuzz_nbt_reader -runs=1000 +``` + +## CI (ClusterFuzzLite) + +The `ClusterFuzzLite` workflow builds and runs these targets on schedule and on PRs. + +## CI (Python/JS fuzzing) + +- `Python Atheris Fuzz` runs `fuzz/meta/fuzz_meta_models.py` +- `JS Fast-Check Fuzz` runs `bot/fuzz/fastcheck.test.js` diff --git a/archived/projt-launcher/docs/README.md b/archived/projt-launcher/docs/README.md new file mode 100644 index 0000000000..3635a2b696 --- /dev/null +++ b/archived/projt-launcher/docs/README.md @@ -0,0 +1,19 @@ +# Documentation + +ProjT Launcher documentation. + +## Sections + +- [Contributing](./contributing/) — Contribution guidelines +- [Handbook](./handbook/) — Developer reference +- [Packaging](./packaging/) — Platform-specific packaging + +## Quick Links + +| Topic | Location | +|-------|----------| +| Getting started | [contributing/GETTING_STARTED.md](./contributing/GETTING_STARTED.md) | +| Code style | [contributing/CODE_STYLE.md](./contributing/CODE_STYLE.md) | +| Architecture | [contributing/ARCHITECTURE.md](./contributing/ARCHITECTURE.md) | +| Third-party libs | [handbook/third-party.md](./handbook/third-party.md) | +| CI workflows | [handbook/workflows.md](./handbook/workflows.md) | diff --git a/archived/projt-launcher/docs/architecture/OVERVIEW.md b/archived/projt-launcher/docs/architecture/OVERVIEW.md new file mode 100644 index 0000000000..7233fdb38a --- /dev/null +++ b/archived/projt-launcher/docs/architecture/OVERVIEW.md @@ -0,0 +1,166 @@ +# Architecture Overview (Detailed) + +## Scope and goals + +This document is a map of the launcher at the module level. It focuses on boundaries, interactions, and the main data flows so you can find the right files quickly before making changes. + +## Layered model + +1. **UI + ViewModels** (`launcher/ui/`, `launcher/viewmodels/`) + - Qt Widgets screens, dialogs, and widgets. + - ViewModels expose state and actions for the UI. + +2. **Core/domain** (`launcher/`, `launcher/minecraft/`, `launcher/java/`) + - Models, settings, instance management, launch logic. + - No UI dependencies. + +3. **Task system** (`launcher/tasks/`) + - Long-running or async work (downloads, extraction, indexing). + - Emits progress and completion signals. + +4. **Networking** (`launcher/net/`) + - HTTP requests and API adapters. + - Used by tasks and core services. + +5. **Mod platform integrations** (`launcher/modplatform/`) + - Mod platform APIs and install flows. + - Orchestrated by tasks and core logic. + +## Where to start in code + +- `launcher/main.cpp`: entry point, application wiring. +- `launcher/Application.cpp`: lifecycle, settings, instance loading. +- `launcher/ui/MainWindow.cpp`: main Qt Widgets UI. +- `launcher/LaunchController.cpp`: launch orchestration (Task). +- `launcher/tasks/`: long-running work and progress reporting. + +## Component catalog + +| Component | Path | Responsibility | Depends on | +| --- | --- | --- | --- | +| UI | `launcher/ui/` | Render UI, collect input, start actions | Core, Tasks | +| ViewModels | `launcher/viewmodels/` | Present core data to the UI | Core | +| Core/Domain | `launcher/`, `launcher/minecraft/` | Domain models and launch logic | Storage, Tasks, Net | +| Tasks | `launcher/tasks/` | Async work and progress reporting | Net, Storage | +| Net | `launcher/net/` | HTTP/APIs, downloads | External APIs | +| Modplatform | `launcher/modplatform/` | Mod platform workflows | Net, Tasks | +| Java | `launcher/java/` | Java discovery/metadata | Storage | + +## High-level control flow + +### Startup + +1. `main.cpp` initializes the `Application` singleton. +2. `Application` loads settings, accounts, and instances. +3. UI is created (`MainWindow`), which binds to application state. + +### Launch flow (typical) + +```mermaid +sequenceDiagram + participant U as User + participant UI as UI (Qt Widgets) + participant C as Core + participant T as LaunchController/Tasks + participant N as Net + participant S as Storage + + U->>UI: Click "Launch" + UI->>C: Build LaunchRequest + C->>T: Start LaunchController (Task) + T->>N: Fetch assets/metadata + N-->>T: Responses + T->>S: Write/update files + T-->>UI: progress + completed +``` + +## Data flow diagram + +```mermaid +graph TD + UI[UI (Qt Widgets)] -->|signals/slots| ViewModels + ViewModels --> Core + UI --> Tasks + Core --> Storage[(Disk: settings/instances/cache)] + Tasks --> Net + Tasks --> Storage + Net --> ExternalAPIs[(Remote APIs)] + Modplatform --> Net +``` + +## Module boundaries and rules + +- UI must not perform long-running work or file/network I/O. +- Core and tasks must not depend on Qt Widgets. +- ViewModels should stay free of UI widget dependencies. +- Use `Task` for work that blocks or takes more than a few milliseconds. +- Keep dependencies flowing "downward": `ui` -> `core` -> `data` (storage/net). + +## Internal contracts (templates) + +Use these templates when documenting module interfaces in feature-specific docs. + +### Task contract + +```cpp +class ExampleTask : public Task { + Q_OBJECT +protected: + void executeTask() override { + setStatus("Working..."); + setProgress(0); + + // Work goes here... + if (failed) { + emitFailed("reason"); + return; + } + + emitSucceeded(); + } +}; +``` + +### Service contract (UI to Core) + +```cpp +struct LaunchRequest { + QString instanceId; + QString javaPath; + QMap env; +}; + +struct LaunchResult { + bool ok; + QString errorCode; + QString message; +}; + +class ILaunchService : public QObject { + Q_OBJECT +public: + virtual Task::Ptr launch(const LaunchRequest& req) = 0; +signals: + void progress(int percent); + void finished(const LaunchResult& result); +}; +``` + +## API contract table (example format) + +| Contract | Producer | Consumer | Request | Response/Event | Errors | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| LaunchTask | Core | Task System | `LaunchRequest` | `progress`, `finished` | `failed(code,msg)` | Async | +| DownloadAsset | Task | Net | URL + headers | bytes + status | network errors | Retry policy | + +## How to update this document + +1. Identify the feature or module (`launcher//`). +2. List the public interfaces it exposes (signals, task APIs, service methods). +3. Add or update the relevant diagram(s) and contract table entries. +4. Verify control flow against entry points in `main.cpp`, `Application`, and `MainWindow`. + +## Notes + +- Diagrams are high-level and may omit file-level details. +- Contracts are templates; validate names and arguments against the current code. diff --git a/archived/projt-launcher/docs/contributing/ARCHITECTURE.md b/archived/projt-launcher/docs/contributing/ARCHITECTURE.md new file mode 100644 index 0000000000..a83334e604 --- /dev/null +++ b/archived/projt-launcher/docs/contributing/ARCHITECTURE.md @@ -0,0 +1,177 @@ +# Architecture + +High-level design and component interaction in ProjT Launcher. + +--- + +## Layers + +``` +┌─────────────────────────────────────────┠+│ UI Layer │ +│ launcher/ui/ (Qt Widgets) │ +├─────────────────────────────────────────┤ +│ Core Layer │ +│ launcher/minecraft/, net/, java/ │ +├─────────────────────────────────────────┤ +│ Task System │ +│ launcher/tasks/ │ +└─────────────────────────────────────────┘ +``` + +### UI Layer + +**Location**: `launcher/ui/` + +- Qt Widgets interface +- Renders state, handles user input +- No direct I/O or network access + +### Core Layer + +**Location**: `launcher/`, `launcher/minecraft/`, `launcher/net/`, `launcher/java/` + +- Business logic +- Data models +- Network operations +- No UI dependencies + +### Task System + +**Location**: `launcher/tasks/` + +- Long-running operations +- Async work (downloads, extraction) +- Progress reporting + +--- + +## Key Principles + +### Separation of Concerns + +UI classes display state and forward user intent. They do not perform: + +- File I/O +- Network requests +- Long computations + +### Communication Pattern + +UI communicates with core via signals/slots: + +```cpp +auto task = makeShared(url); +connect(task.get(), &Task::progress, this, &MainWindow::updateProgress); +connect(task.get(), &Task::finished, this, &MainWindow::onDownloadComplete); +task->start(); +``` + +### Threading + +| Thread | Purpose | +|--------|---------| +| Main | UI rendering only | +| Worker | File I/O, networking, hashing | + +Operations > 10ms should be async. + +--- + +## Task System + +### Creating a Task + +```cpp +class MyTask : public Task { + Q_OBJECT + +protected: + void executeTask() override { + setStatus("Working..."); + setProgress(0); + + // Do work + for (int i = 0; i < 100; i++) { + if (isCancelled()) { + emitFailed("Cancelled"); + return; + } + setProgress(i); + } + + emitSucceeded(); + } +}; +``` + +### Running a Task + +```cpp +auto task = makeShared(); +connect(task.get(), &Task::succeeded, this, &MyClass::onSuccess); +connect(task.get(), &Task::failed, this, &MyClass::onFailure); +task->start(); +``` + +--- + +## Application Lifecycle + +1. **Startup**: `main.cpp` creates `Application` singleton +2. **Setup**: Load settings, accounts, instances +3. **UI Launch**: Create `MainWindow` +4. **Runtime**: Event loop processes user actions +5. **Shutdown**: Save state, release resources + +--- + +## Service Objects + +Long-lived non-UI classes owned by Application: + +- `AccountManager` - Microsoft/offline accounts +- `InstanceManager` - Minecraft instances +- `NetworkManager` - HTTP operations +- `SettingsManager` - Configuration + +Access via `Application::instance()->serviceName()`. + +--- + +## Common Violations + +**Don't do these**: + +| Violation | Fix | +|-----------|-----| +| `sleep()` in UI | Use Task | +| Network in UI class | Move to core service | +| UI import in core | Remove dependency | +| Direct file I/O in UI | Use Task | + +--- + +## Module Dependencies + +``` +ui/ ──→ minecraft/ ──→ net/ + │ │ + └──→ tasks/ â†â”€â”€â”˜ + │ + └──→ java/ +``` + +**Rules**: + +- No circular dependencies +- `ui/` depends on everything +- Core modules are independent +- `tasks/` is a utility layer + +--- + +## Related + +- [Project Structure](./PROJECT_STRUCTURE.md) +- [Testing](./TESTING.md) diff --git a/archived/projt-launcher/docs/contributing/CODE_STYLE.md b/archived/projt-launcher/docs/contributing/CODE_STYLE.md new file mode 100644 index 0000000000..b51725a92d --- /dev/null +++ b/archived/projt-launcher/docs/contributing/CODE_STYLE.md @@ -0,0 +1,230 @@ +# Code Style + +Formatting and coding standards for ProjT Launcher. + +--- + +## Formatting + +All C++ code must be formatted with clang-format before committing. + +```sh +clang-format -i path/to/file.cpp +``` + +CI will reject unformatted code. + +--- + +## C++ Standards + +### Modern C++ + +| Feature | Usage | +|---------|-------| +| `auto` | Only when type is obvious | +| `nullptr` | Always (never `NULL` or `0`) | +| `override` | Required on all overrides | +| `const` | Required for non-mutating methods | + +### Memory Management + +**Smart Pointers**: + +```cpp +// Good +auto obj = std::make_unique(); + +// Bad - raw pointer ownership +MyClass* obj = new MyClass(); +``` + +**Qt Parent Ownership**: + +```cpp +// Good - Qt manages lifetime +new QButton(this); +``` + +### Error Handling + +- Avoid exceptions +- Use `std::optional` for missing values +- Use `std::expected` for operations that can fail + +### Lambdas + +```cpp +// Good - explicit capture +connect(btn, &QPushButton::clicked, this, [this, id]() { + handleClick(id); +}); + +// Bad - default capture +connect(btn, &QPushButton::clicked, [=]() { ... }); +``` + +--- + +## Naming Conventions + +| Type | Format | Example | +|------|--------|---------| +| Class | PascalCase | `MainWindow` | +| Private member | `m_` + camelCase | `m_currentTheme` | +| Static member | `s_` + camelCase | `s_instance` | +| Public member | camelCase | `dateOfBirth` | +| Constant | SCREAMING_SNAKE | `MAX_VALUE` | +| Function | camelCase | `getCurrentTheme()` || Enum class | PascalCase | `enum class State` | +| Enum values | PascalCase | `State::Running` | + +**SCREAMING_SNAKE scope**: Use for `constexpr` constants, `#define` macros, and legacy `enum` values only. Prefer `enum class` with PascalCase values for new code. +--- + +## Headers + +### Include Guards + +```cpp +#pragma once +``` + +### Include Order + +1. Corresponding header +2. C++ standard library +3. Qt headers +4. Third-party headers +5. Project headers + +```cpp +#include "MyClass.h" + +#include +#include + +#include +#include + +#include "OtherClass.h" +``` + +**Note**: clang-format automatically reorders includes. Always accept its output. + +### Forward Declarations + +Prefer forward declarations over includes when possible: + +```cpp +// Good +class OtherClass; + +// Avoid +#include "OtherClass.h" +``` + +**Qt warning**: Forward declarations do not work for QObject-derived classes that use `Q_OBJECT`. These must be fully included. + +--- + +## Comments + +### Documentation + +```cpp +/// @brief Short description. +/// +/// Detailed description if needed. +/// @param input Description of parameter. +/// @return Description of return value. +bool doSomething(const QString& input); +``` + +### Implementation Comments + +```cpp +// Why this approach was chosen +// NOT what the code does +``` + +### TODOs + +```cpp +// TODO(username): Description of what needs to be done +// FIXME(username): Description of broken behavior that needs fixing +``` + +- **TODO**: Planned improvement or missing feature +- **FIXME**: Known bug or broken behavior requiring attention +- Username is required for traceability +- Compatible with IDE TODO/FIXME highlighting tools + +--- + +## Qt Widgets + +### UI Files + +- Use Qt Designer for layout +- Never edit generated `ui_*.h` files +- Set meaningful `objectName` values + +### Widget Classes + +- Keep UI logic minimal +- Delegate to core services +- Use signals/slots for communication + +--- + +## File Templates + +These templates apply only to new Project Tick–owned files. Do not modify license headers in upstream forked code. + +### Header (.h) + +```cpp +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick + +#pragma once + +#include + +class MyClass : public QObject { + Q_OBJECT + +public: + explicit MyClass(QObject* parent = nullptr); + ~MyClass() override; + +signals: + void somethingHappened(); + +private: + QString m_data; +}; +``` + +### Source (.cpp) + +```cpp +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick + +#include "MyClass.h" + +MyClass::MyClass(QObject* parent) + : QObject(parent) +{ +} + +MyClass::~MyClass() = default; +``` + +--- + +## Related + +- [Project Structure](./PROJECT_STRUCTURE.md) +- [Architecture](./ARCHITECTURE.md) diff --git a/archived/projt-launcher/docs/contributing/GETTING_STARTED.md b/archived/projt-launcher/docs/contributing/GETTING_STARTED.md new file mode 100644 index 0000000000..ac25232f28 --- /dev/null +++ b/archived/projt-launcher/docs/contributing/GETTING_STARTED.md @@ -0,0 +1,222 @@ +# Getting Started + +Guide to setting up your development environment for ProjT Launcher. + +--- + +## Prerequisites + +### Required Tools + +| Tool | Version | Purpose | +|------|---------|---------| +| CMake | 3.22+ | Build system | +| Qt | 6.8.0+ | GUI framework | +| Compiler | C++20 | GCC 11+, Clang 14+, MSVC 2022 | +| Ninja | 1.10+ | Build tool | +| Git | 2.30+ | Version control | + +### Optional Tools + +| Tool | Purpose | +|------|---------| +| clang-format | Code formatting | +| clang-tidy | Static analysis | +| Valgrind | Memory checking (Linux) | + +--- + +## Clone Repository + +```sh +# Fork on GitHub first, then: +git clone https://github.com/YourName/ProjT-Launcher.git +cd ProjT-Launcher + +# Add upstream remote +git remote add upstream https://github.com/Project-Tick/ProjT-Launcher.git +``` + +--- + +## Platform Setup + +### Windows + +**Visual Studio 2022**: + +1. Install VS 2022 with "Desktop development with C++" +2. Install Qt 6.8.0+ via Qt Online Installer + - Select MSVC 2022 64-bit + - Select Qt Shader Tools +3. Install dependencies via NuGet (required for CI parity): + +```powershell +nuget install ./packages.config -OutputDirectory dependencies +``` + +**Note**: NuGet dependencies are mandatory for Windows builds to ensure CI reproducibility. + +### Linux + +**Nix (Recommended)**: + +```sh +nix develop .#default +``` + +**Manual**: + +```sh +# Debian/Ubuntu +sudo apt install cmake ninja-build qt6-base-dev qt6-tools-dev + +# Fedora +sudo dnf install cmake ninja-build qt6-qtbase-devel qt6-qttools-devel + +# Arch +sudo pacman -S cmake ninja qt6-base qt6-tools +``` + +### macOS + +**Nix (Recommended)**: + +```sh +nix develop .#default +``` + +**Homebrew**: + +```sh +brew install cmake ninja qt@6 +``` + +--- + +## Build + +### Configure + +```sh +# List available presets +cmake --list-presets + +# Windows +cmake --preset windows_msvc + +# Linux +cmake --preset linux + +# macOS +cmake --preset macos +``` + +### Build + +```sh +# Debug (development) +cmake --build --preset --config Debug + +# Release (production) +cmake --build --preset --config Release +``` + +### Run + +```sh +# Windows (MSVC preset) +./build/windows_msvc/Debug/ProjT-Launcher.exe + +# Linux +./build/linux/Debug/ProjT-Launcher + +# macOS +./build/macos/Debug/ProjT-Launcher.app/Contents/MacOS/ProjT-Launcher +``` + +### Test + +```sh +ctest --preset --output-on-failure +``` + +--- + +## IDE Setup + +### VS Code (Recommended) + +Extensions: +- C/C++ (Microsoft) +- CMake Tools (Microsoft) +- clangd (LLVM) + +Settings (`.vscode/settings.json`): + +```json +{ + "cmake.configureOnOpen": true, + "cmake.generator": "Ninja" +} +``` + +### Qt Creator + +1. Open `CMakeLists.txt` as project +2. Select Qt 6.8.0+ kit +3. Import `.clang-format` in Tools > Options > C++ > Code Style + +### Visual Studio 2022 + +1. File > Open > CMake +2. Select `CMakeLists.txt` +3. Configure Qt path in CMake settings + +--- + +## Troubleshooting + +### Qt not found + +```sh +cmake --preset -DCMAKE_PREFIX_PATH="/path/to/Qt/6.8.0/gcc_64" +``` + +### Wrong Qt minor version + +CI requires exact Qt version match. Check `cmake/versions.cmake` for the required version. Mismatched Qt versions cause ABI incompatibility. + +### MSVC vs MinGW mismatch + +Qt kit must match your compiler. Use MSVC kit with Visual Studio, MinGW kit with MinGW toolchain. Do not mix. + +### clang-format version mismatch + +Use the version specified in CI. Different versions produce different output. Check `.gitlab-ci.yml` and `.gitlab/ci/` for the expected version. + +### Ninja not found + +Install Ninja or use a different generator: + +```sh +cmake -G "Unix Makefiles" .. +``` + +### C++20 not supported + +Update your compiler: +- GCC: 11+ +- Clang: 14+ +- MSVC: 2022 + +--- + +## Next Steps + +These documents must be read before submitting code: + +- [Code Style](./CODE_STYLE.md) - Formatting rules +- [Project Structure](./PROJECT_STRUCTURE.md) - Where files go +- [Architecture](./ARCHITECTURE.md) - How components interact +- [Workflow](./WORKFLOW.md) - Git workflow and PR process diff --git a/archived/projt-launcher/docs/contributing/LAUNCHER_TEST_MATRIX.md b/archived/projt-launcher/docs/contributing/LAUNCHER_TEST_MATRIX.md new file mode 100644 index 0000000000..119dbd5cb7 --- /dev/null +++ b/archived/projt-launcher/docs/contributing/LAUNCHER_TEST_MATRIX.md @@ -0,0 +1,51 @@ +# Launcher Test Matrix (UI Excluded) + +This document defines the **test coverage expectations** for all non-UI subsystems under `launcher/`. + +## Scope + +- **Included:** All core subsystems under `launcher/` +- **Excluded:** `launcher/ui/` (UI tests are tracked separately) + +## Goals + +- Each subsystem has at least **one unit/integration test** +- New or changed core behavior must be covered by tests +- All tests run via `ctest` +- Tests are platform-agnostic (Linux/Windows/macOS) + +## Subsystem Coverage List + +The table below shows the **target coverage**. “Covered†means baseline tests exist for the subsystem (UI excluded). + +| Subsystem | Path(s) | Example Tests | Status | +|---|---|---|---| +| File system & archives | `launcher/FileSystem.*`, `launcher/GZip.*`, `launcher/MMCZip.*`, `launcher/Untar.*` | `FileSystem_test`, `GZip_test` | Covered | +| Versioning | `launcher/Version.*` | `Version_test` | Covered | +| String/Json helpers | `launcher/StringUtils.*`, `launcher/Json.*` | `StringUtils_test`, `Json_test` | Covered | +| SeparatorPrefixTree | `launcher/SeparatorPrefixTree.h` | `SeparatorPrefixTree_test` | Covered | +| INI/Config | `launcher/INIFile.*` | `INIFile_test` | Covered | +| Task system | `launcher/tasks/` | `Task_test` | Covered | +| Java management | `launcher/java/` | `JavaVersion_test`, `RuntimeVersion_test` | Covered | +| Minecraft pack parsing | `launcher/minecraft/mod/` | `DataPackParse_test`, `ResourcePackParse_test`, `TexturePackParse_test`, `ShaderPackParse_test`, `WorldSaveParse_test` | Covered | +| Minecraft version formats | `launcher/minecraft/` | `MojangVersionFormat_test`, `Version_test` | Covered | +| Library/Dependency | `launcher/minecraft/Library.*`, `launcher/GradleSpecifier.*` | `Library_test`, `GradleSpecifier_test` | Covered | +| Mod platforms | `launcher/modplatform/` | `ModPlatform_test` | Covered | +| Networking/Downloads | `launcher/net/` | `NetUtils_test`, `NetHeaderProxy_test`, `NetSink_test` | Covered | +| Launch pipeline | `launcher/launch/`, `launcher/LaunchController.*` | `LaunchVariableExpander_test`, `LaunchLogModel_test`, `LaunchLineRouter_test`, `LaunchPipeline_test` | Covered | +| Instance management | `launcher/Instance*.*`, `launcher/BaseInstance.*` | `InstanceCopyPrefs_test`, `BaseInstanceSettings_test` | Covered | +| Updater | `launcher/updater/` | `ProjTExternalUpdater_test` | Covered | +| Logs/Diagnostics | `launcher/logs/`, `launcher/XmlLogs.*` | `XmlLogs_test`, `LogEventParser_test`, `MessageLevel_test` | Covered | +| Meta parsing | `launcher/meta/` | `MetaComponentParse_test` | Covered | +| Resource model | `launcher/minecraft/ResourceFolderModel.*` | `ResourceFolderModel_test` | Covered | + +## Rules For New Tests + +- New subsystems must add a row here +- “Missing†or “Covered†areas require new tests +- UI tests are tracked separately + +## CI Expectation + +- All tests run via `ctest` +- CI runs the same test set on all platforms diff --git a/archived/projt-launcher/docs/contributing/PROJECT_STRUCTURE.md b/archived/projt-launcher/docs/contributing/PROJECT_STRUCTURE.md new file mode 100644 index 0000000000..17cef52415 --- /dev/null +++ b/archived/projt-launcher/docs/contributing/PROJECT_STRUCTURE.md @@ -0,0 +1,123 @@ +# Project Structure + +Directory layout and file placement guide. + +--- + +## Directory Layout + +``` +ProjT-Launcher/ +├── launcher/ # Main application +│ ├── ui/ # Qt Widgets +│ │ ├── pages/ # Main screens +│ │ ├── widgets/ # Reusable components +│ │ ├── dialogs/ # Modal windows +│ │ └── setupwizard/ # First-run wizard +│ ├── minecraft/ # Game logic +│ │ ├── auth/ # Account authentication +│ │ ├── launch/ # Game process +│ │ ├── mod/ # Mod loading +│ │ └── versions/ # Version parsing +│ ├── net/ # Networking +│ ├── tasks/ # Background jobs +│ ├── java/ # Java runtime +│ ├── modplatform/ # Mod platform APIs +│ ├── resources/ # Images, themes +│ ├── icons/ # App icons +│ └── translations/ # Language files +├── tests/ # Unit tests +├── cmake/ # Build configuration +└── docs/ # Documentation +``` + +--- + +## File Placement + +### C++ Files + +| Location | Purpose | +|----------|---------| +| `launcher/ui/` | Qt Widgets UI | +| `launcher/minecraft/` | Game logic | +| `launcher/net/` | HTTP, downloads | +| `launcher/tasks/` | Async operations | +| `launcher/modplatform/` | Modrinth, CurseForge | + +**Note**: `minecraft/` is for Minecraft-specific logic only (versions, mods, launch process). Generic launcher functionality belongs in `launcher/` or appropriate submodules. + +### UI Files + +| Location | Purpose | +|----------|---------| +| `launcher/ui/widgets/` | Reusable widgets | +| `launcher/ui/pages/` | Main screens | +| `launcher/ui/dialogs/` | Popups, modals | +| `launcher/ui/setupwizard/` | First-run flow | + +### Assets + +| Location | Format | +|----------|--------| +| `launcher/resources/` | PNG, SVG | +| `launcher/icons/` | ICO, PNG, SVG | +| `launcher/translations/` | .ts | + +--- + +## Quick Reference + +| I want to add... | Location | +|------------------|----------| +| New screen | `launcher/ui/pages/` | +| Reusable widget | `launcher/ui/widgets/` | +| Modal dialog | `launcher/ui/dialogs/` | +| Network API | `launcher/net/` | +| Background job | `launcher/tasks/` | +| Game logic | `launcher/minecraft/` | +| Unit test | `tests/` | + +--- + +## Naming + +| Type | Convention | Example | +|------|------------|---------| +| C++ class | PascalCase | `InstanceList.cpp` | +| UI file | PascalCase | `SettingsPage.ui` | +| Asset | kebab-case | `app-icon.png` | +| Test | PascalCase_test | `FileSystem_test.cpp` | + +--- + +## Rules + +- No circular dependencies between modules +- `ui/` → `core` → `data` layering (conceptual layers, not directory names) +- Tests mirror source structure +- Do not create new top-level directories without maintainer approval + +--- + +## Third-Party Libraries + +Location: Root directory (e.g., `zlib/`, `quazip/`) + +All third-party code is maintained as detached forks. See [third-party.md](../handbook/third-party.md) for the complete list, upstream references, and patch policies. + +--- + +## Test Scope + +- `tests/` contains unit tests primarily +- Integration tests go in `tests/` but must be clearly named +- UI tests are discouraged; prefer testing core logic +- See [TESTING.md](./TESTING.md) for test standards + +--- + +## Related + +- [Architecture](./ARCHITECTURE.md) +- [Testing](./TESTING.md) diff --git a/archived/projt-launcher/docs/contributing/README.md b/archived/projt-launcher/docs/contributing/README.md new file mode 100644 index 0000000000..14a9f5584d --- /dev/null +++ b/archived/projt-launcher/docs/contributing/README.md @@ -0,0 +1,19 @@ +# Contributing Guide + +Documentation for developers who want to contribute to ProjT Launcher. + +## Getting Started + +- [Getting Started](./GETTING_STARTED.md) - Environment setup +- [Code Style](./CODE_STYLE.md) - Formatting and conventions +- [Workflow](./WORKFLOW.md) - Git workflow and DCO + +## Reference + +- [Architecture](./ARCHITECTURE.md) - Code organization +- [Project Structure](./PROJECT_STRUCTURE.md) - Directory layout +- [Testing](./TESTING.md) - Test standards + +--- + +See also: [Developer Handbook](../handbook/) diff --git a/archived/projt-launcher/docs/contributing/TESTING.md b/archived/projt-launcher/docs/contributing/TESTING.md new file mode 100644 index 0000000000..7acb1dd55e --- /dev/null +++ b/archived/projt-launcher/docs/contributing/TESTING.md @@ -0,0 +1,149 @@ +# Testing + +Testing standards and practices. + +**Framework**: QtTest is the only supported test framework. Do not introduce other frameworks. + +--- + +## Requirements + +| Change Type | Test Required | +|-------------|---------------| +| New logic | Yes | +| Bug fix | Yes (regression) | +| Refactor | Recommended | +| UI layout | No | +| Docs | No | + +--- + +## Test Structure + +```cpp +#include +#include "MyClass.h" + +class MyClassTest : public QObject { + Q_OBJECT + +private slots: + void initTestCase(); // Before all tests + void cleanupTestCase(); // After all tests + void init(); // Before each test + void cleanup(); // After each test + + void test_methodName_condition(); +}; + +QTEST_GUILESS_MAIN(MyClassTest) +#include "MyClassTest.moc" +``` + +--- + +## Naming + +- **File**: `ClassName_test.cpp` +- **Class**: `ClassNameTest` +- **Method**: `test_method_condition` + +--- + +## Async Testing + +Use `QSignalSpy` instead of `sleep()`: + +```cpp +QSignalSpy spy(obj, &MyClass::finished); +obj->startAsync(); +QVERIFY(spy.wait(1000)); +QCOMPARE(spy.count(), 1); +``` + +--- + +## Mocking + +Create fake implementations for external dependencies: + +```cpp +class FakeNetworkService : public INetworkService { +public: + void get(const QString& url) override { + emit finished("Fake Data"); + } +}; +``` + +**Forbidden practices**: + +- Real network requests +- Real file system access outside `QTemporaryDir` +- User accounts or credentials +- `sleep()` or timing-based waits (use `QSignalSpy::wait()`) +- System-specific state dependencies + +## UI Testing + +UI tests are generally discouraged. Prefer testing core logic via unit tests. + +If UI testing is necessary: + +- Keep scope minimal (widget behavior only) +- Do not test layout or visual appearance +- Use `QTest::mouseClick()` and `QTest::keyClick()` for interaction +- Isolate widgets from services using mocks + +--- + +## Performance + +- Single test: < 100ms +- Mark slow tests clearly +- Use `QTemporaryDir` for file tests + +--- + +## Running Tests + +**VS Code**: Testing tab → Run + +**Command line (with presets)**: + +```bash +ctest --preset default --output-on-failure +``` + +**Without presets**: + +```bash +cd build +ctest --output-on-failure +``` + +**Single test**: + +```bash +./build//tests/MyClassTest +``` + +--- + +## CI + +- All tests run via `ctest` +- Failing tests block merge +- Must pass on all platforms + +## Launcher Coverage + +- UI is excluded from core coverage requirements +- See the launcher subsystem test matrix: [Launcher Test Matrix](./LAUNCHER_TEST_MATRIX.md) + +--- + +## Related + +- [Architecture](./ARCHITECTURE.md) +- [Project Structure](./PROJECT_STRUCTURE.md) diff --git a/archived/projt-launcher/docs/contributing/WORKFLOW.md b/archived/projt-launcher/docs/contributing/WORKFLOW.md new file mode 100644 index 0000000000..2712f089e8 --- /dev/null +++ b/archived/projt-launcher/docs/contributing/WORKFLOW.md @@ -0,0 +1,142 @@ +# Workflow + +Git workflow and contribution process. + +--- + +## Branches + +| Branch | Purpose | +|--------|---------| +| `develop` | Integration branch (protected) | +| `release-X.Y.Z` | Release preparation | +| `hotfix/X.Y.Z` | Critical bug fixes | + +Never push directly to `develop`. + +--- + +## Commit Messages + +We use Conventional Commits: + +``` +(): + +Signed-off-by: Name +``` + +### Types + +| Type | Use | +|------|-----| +| `feat` | New feature | +| `fix` | Bug fix | +| `docs` | Documentation | +| `style` | Formatting | +| `refactor` | Code change (no behavior change) | +| `perf` | Performance | +| `test` | Tests | +| `chore` | Build/tooling | + +### Example + +``` +feat(ui): add dark mode toggle + +Signed-off-by: John Doe +``` + +**Forbidden**: Do not mix scopes in a single commit. A commit touching both `ui/` and `minecraft/` must be split. + +--- + +## DCO (Sign-off) + +Every commit must be signed: + +```bash +git commit -s -m "feat: my feature" +``` + +Forgot sign-off? + +```bash +git commit --amend -s --no-edit +git push --force-with-lease +``` + +--- + +## Pull Requests + +1. **One feature per PR** +2. **Merge commit only** (no squash, no rebase) +3. **CI must pass** +4. **One maintainer approval required** + +**Why merge commits?** History preservation and review traceability. Each commit remains linked to its PR and review discussion. + +--- + +## Merge Conflicts + +```bash +git fetch origin +git rebase origin/develop +# Fix conflicts +git add . +git rebase --continue +git push --force-with-lease +``` + +**Rebase policy**: Rebase is allowed only on local branches before PR submission. Never rebase protected branches or branches with open PRs. + +--- + +## Release Process + +1. Freeze `develop` +2. Create `release-X.Y.Z` +3. QA testing +4. Bug fixes to release branch +5. Merge to `develop` +6. Tag release + +--- + +## License Headers + +### Project Tick–Owned Files + +```cpp +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + */ +``` + +### Upstream Forked Files + +Do not modify existing license headers in forked code. When adding substantial changes, add a comment block below the original header: + +```cpp +// Modifications by Project Tick, 2026 +``` + +### Modified Files + +Preserve original copyright notices and add Project Tick copyright. + +--- + +## Related + +- [Getting Started](./GETTING_STARTED.md) +- [Code Style](./CODE_STYLE.md) diff --git a/archived/projt-launcher/docs/handbook/README.md b/archived/projt-launcher/docs/handbook/README.md new file mode 100644 index 0000000000..785215e6ac --- /dev/null +++ b/archived/projt-launcher/docs/handbook/README.md @@ -0,0 +1,65 @@ +# Handbook + +Documentation for Project Tick contributors and players. +Latest Version: 0.0.5-1 + +## For Players + +- [help-pages/index.md](./help-pages/index.md) - Launcher help (settings, instances, mod platforms) +- [wiki/overview/index.md](./wiki/overview/index.md) - Full ProjT Launcher wiki (mirrored) + +## For Contributors + +### Core Components + +- [program_info.md](./program_info.md) - Application branding and configuration +- [launcherjava.md](./launcherjava.md) - Java launcher component +- [javacheck.md](./javacheck.md) - Java runtime detection + +### Detached Fork Libraries + +Libraries independently maintained by Project Tick. + +#### Compression + +- [zlib.md](./zlib.md) - DEFLATE compression +- [bzip2.md](./bzip2.md) - Block-sorting compression +- [quazip.md](./quazip.md) - Qt ZIP wrapper + +#### Data Formats + +- [tomlplusplus.md](./tomlplusplus.md) - TOML parser +- [libnbtplusplus.md](./libnbtplusplus.md) - NBT format +- [cmark.md](./cmark.md) - Markdown parser +- [libqrencode.md](./libqrencode.md) - QR code generation + +### Build System + +- [extra-cmake-modules.md](./extra-cmake-modules.md) - KDE CMake modules +- [bzip2-compiling.md](./bzip2-compiling.md) - bzip2 build instructions +- [bzip2-tests.md](./bzip2-tests.md) - bzip2 test suite +- [bzip2-testfiles.md](./bzip2-testfiles.md) - Test file collection + +### CI/CD & Automation + +- [workflows.md](./workflows.md) - GitHub Actions architecture +- [ci_support.md](./ci_support.md) - CI configuration files +- [bot.md](./bot.md) - PR automation bot +- [ptcigh.md](./ptcigh.md) - GitHub script helpers +- [ptcieval.md](./ptcieval.md) - Nix-based validation + +### Platform Support + +- [nix.md](./nix.md) - Nix/NixOS packaging +- [linux-packaging.md](./linux-packaging.md) - Linux distribution packaging +- [images.md](./images.md) - CI Docker images + +### Reference + +- [third-party.md](./third-party.md) - All Libraries +- [website-tomlplusplus.md](./website-tomlplusplus.md) - toml++ docs build +- [help-pages.md](./help-pages.md) - Help pages overview and locations + +--- + +See also: [Contributing Guide](../../CONTRIBUTING.md) diff --git a/archived/projt-launcher/docs/handbook/bot.md b/archived/projt-launcher/docs/handbook/bot.md new file mode 100644 index 0000000000..807f1da838 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/bot.md @@ -0,0 +1,198 @@ +# Bot `bot/` + +> **Type**: Cloudflare Worker +> **Platform**: Cloudflare Workers +> **Purpose**: PR Automation & Labeling +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +The ProjT Launcher bot is a Cloudflare Worker that automates pull request labeling based on changed files. It listens for GitHub webhook events and applies appropriate labels automatically. + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| **Auto-labeling** | Labels PRs based on file changes | +| **Path mapping** | Configurable path-to-label rules | +| **Webhook integration** | GitHub webhook receiver | +| **Zero cold start** | Cloudflare Workers edge deployment | + +--- + +## Architecture + +``` +┌─────────────┠┌──────────────────┠┌─────────────┠+│ GitHub │────▶│ Cloudflare │────▶ │ GitHub │ +│ Webhook │ │ Worker (bot/) │ │ API │ +└─────────────┘ └──────────────────┘ └─────────────┘ + │ + ┌──────▼────────┠+ │ wrangler.json │ + │ (config) │ + └───────────────┘ +``` + +--- + +## File Structure + +``` +bot/ +├── index.js # Main worker logic +├── server.js # Local development server +├── package.json # Dependencies +└── wrangler.json # Cloudflare configuration +``` + +--- + +## Label Rules + +The bot applies labels based on changed file paths: + +| Path Pattern | Label | +|--------------|-------| +| `launcher/**` | `launcher` | +| `buildconfig/**` | `build` | +| `cmake/**` | `build` | +| `.github/workflows/**` | `ci` | +| `docs/**` | `documentation` | +| `zlib/**` | `library: zlib` | +| `bzip2/**` | `library: bzip2` | +| `quazip/**` | `library: quazip` | +| `cmark/**` | `library: cmark` | +| `tomlplusplus/**` | `library: toml++` | +| `libqrencode/**` | `library: qrencode` | +| `website/**` | `website` | + +--- + +## Configuration + +### wrangler.json + +```json +{ + "name": "projtlauncher-bot", + "main": "index.js", + "compatibility_date": "2024-01-01", + "vars": { + "GITHUB_APP_ID": "your-app-id" + } +} +``` + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `GITHUB_APP_ID` | GitHub App ID | +| `GITHUB_APP_PRIVATE_KEY` | GitHub App private key | +| `GITHUB_WEBHOOK_SECRET` | Webhook signature secret | + +--- + +## Development + +### Prerequisites + +- Node.js 18+ +- Wrangler CLI (`npm install -g wrangler`) +- Cloudflare account + +### Local Development + +```bash +cd bot +npm install + +# Start local dev server +npm run dev +# or +wrangler dev +``` + +### Testing Webhooks Locally + +Use [smee.io](https://smee.io/) or ngrok to forward webhooks: + +```bash +# Install smee client +npm install -g smee-client + +# Forward webhooks +smee -u https://smee.io/your-channel -t http://localhost:8787 +``` + +--- + +## Deployment + +### Deploy to Cloudflare + +```bash +cd bot +wrangler publish +``` + +### GitHub Webhook Setup + +1. Go to repository Settings → Webhooks +2. Add webhook: + - **Payload URL**: `https://your-worker.workers.dev/webhook` + - **Content type**: `application/json` + - **Secret**: Your `GITHUB_WEBHOOK_SECRET` + - **Events**: Pull requests + +--- + +## API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/` | GET | Health check | +| `/webhook` | POST | GitHub webhook receiver | + +--- + +## Security + +- ✅ **Signature verification** — HMAC-SHA256 webhook signatures +- ✅ **App authentication** — GitHub App JWT tokens +- ✅ **Secret management** — Cloudflare Workers secrets + +--- + +## Troubleshooting + +### Bot not labeling PRs + +1. Check Cloudflare Workers logs +2. Verify webhook delivery in GitHub settings +3. Ensure secrets are configured correctly + +### Labels not created + +Labels must exist in the repository before the bot can apply them. + +--- + +## Related Documentation + +- [CI Workflows](./workflows.md) — GitHub Actions integration +- [GitHub Scripts](./ptcigh.md) — Additional automation + +--- + +## External Links + +- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) +- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) +- [GitHub Apps](https://docs.github.com/en/apps) +- [GitHub Webhooks](https://docs.github.com/en/webhooks) diff --git a/archived/projt-launcher/docs/handbook/bzip2-compiling.md b/archived/projt-launcher/docs/handbook/bzip2-compiling.md new file mode 100644 index 0000000000..7b3afb5c51 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/bzip2-compiling.md @@ -0,0 +1,163 @@ +# Compiling Bzip2 + +> **Directory**: `bzip2/` +> **Build Systems**: Meson (preferred), CMake +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +Bzip2 supports two build systems. **Meson** is preferred for Unix-like systems, while **CMake** provides cross-platform support. + +--- + +## Build Systems Comparison + +| Feature | Meson | CMake | +|---------|-------|-------| +| Unix Support | ✅ Excellent | ✅ Good | +| Windows Support | ✅ Good | ✅ Excellent | +| Preferred For | Unix-like | Cross-platform | + +--- + +## Prerequisites + +### All Platforms + +- Python 3.6+ +- C compiler (GCC, Clang, or MSVC) + +### Meson Builds + +- Meson 0.56+ +- Ninja +- pkg-config + +### CMake Builds + +- CMake 3.12+ + +--- + +## Meson Build + +### Linux/macOS + +```bash +cd bzip2 + +# Configure +meson setup builddir --prefix=/usr + +# Build +ninja -C builddir + +# Test +meson test -C builddir --print-errorlogs + +# Install (optional) +sudo ninja -C builddir install +``` + +### Windows (MSVC) + +```cmd +cd bzip2 + +REM From VS Developer Command Prompt +meson setup builddir +ninja -C builddir +meson test -C builddir --print-errorlogs +``` + +### Meson Options + +| Option | Default | Description | +|--------|---------|-------------| +| `default_library` | `shared` | `static`, `shared`, or `both` | +| `--backend` | `ninja` | Use `vs` for MSBuild | +| `--unity` | off | Enable unity build | +| `buildtype` | `debug` | `release`, `debug`, etc. | + +--- + +## CMake Build + +### Linux/macOS + +```bash +cd bzip2 +mkdir build && cd build + +# Configure +cmake .. -DCMAKE_BUILD_TYPE=Release + +# Build +cmake --build . + +# Test +ctest -V + +# Install (optional) +cmake --install . +``` + +### Windows + +```powershell +cd bzip2 +mkdir build; cd build + +# Configure +cmake .. + +# Build (Release) +cmake --build . --config Release + +# Test +ctest -C Release -V +``` + +### CMake Options + +| Option | Default | Description | +|--------|---------|-------------| +| `ENABLE_APP` | `ON` | Build bzip2 program | +| `ENABLE_LIB_ONLY` | `OFF` | Only build library | +| `ENABLE_STATIC_LIB` | `OFF` | Build static library | +| `ENABLE_SHARED_LIB` | `ON` | Build shared library | +| `ENABLE_TESTS` | `ON` | Build tests | +| `ENABLE_DOCS` | `OFF` | Generate documentation | +| `ENABLE_EXAMPLES` | `OFF` | Build examples | +| `USE_OLD_SONAME` | `OFF` | Use old SONAME (distro compat) | + +--- + +## SONAME Compatibility (Linux) + +The SONAME changed from `libbz2.so.1.0` to `libbz2.so.1` in version 1.1. + +For distro compatibility: + +```bash +# Option 1: CMake flag +cmake .. -DUSE_OLD_SONAME=ON + +# Option 2: Manual patchelf +patchelf --set-soname libbz2.so.1.0 libbz2.so.1.0.9 +``` + +Check SONAME: +```bash +objdump -p libbz2.so.1.0.9 | grep SONAME +``` + +--- + +## Related Documentation + +- [Bzip2 Overview](./bzip2.md) — Main documentation +- [Bzip2 Tests](./bzip2-tests.md) — Test suite +- [Bzip2 Test Files](./bzip2-testfiles.md) — Test file collection diff --git a/archived/projt-launcher/docs/handbook/bzip2-testfiles.md b/archived/projt-launcher/docs/handbook/bzip2-testfiles.md new file mode 100644 index 0000000000..8631c937b4 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/bzip2-testfiles.md @@ -0,0 +1,101 @@ +# Bzip2 Test Files + +> **Purpose**: Test file collection for bzip2 validation +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +A collection of `.bz2` files used to test bzip2 functionality. Files come from various projects and cover edge cases. + +--- + +## Directory Contents + +Each test directory contains: + +| File Type | Description | +|-----------|-------------| +| `README` | Source and license information | +| `*.bz2` | Compressed files (should work) | +| `*.md5` | Hash of original file | +| `*.bz2.bad` | Corrupt files (should fail) | + +--- + +## File Types + +### Good Files (`.bz2`) + +Compressed files that should decompress correctly. Each has an `.md5` file for verification. + +### Bad Files (`.bz2.bad`) + +Deliberately corrupt files to test error handling. These should **not** decompress successfully. + +--- + +## Verification + +### Generate MD5 + +```bash +md5sum < file > file.md5 +``` + +### Verify MD5 + +```bash +md5sum --check file.md5 < file +``` + +--- + +## Adding Test Files + +### Good Files + +1. Create the `.bz2` file +2. Generate `.md5`: `md5sum < original > original.md5` +3. Add to appropriate directory +4. Update README with source + +### Bad Files + +1. Create corrupt `.bz2` file +2. Name with `.bz2.bad` extension +3. Document the corruption type + +--- + +## âš ï¸ Security Notice + +> **Vulnerability Reporting** +> +> If you find a file that causes crashes, buffer overflows, or security issues: +> +> 1. **Do NOT share publicly** +> 2. Report confidentially to maintainers +> 3. Allow 60-90 days for fix before disclosure + +--- + +## Credits + +Test files contributed by: +- Apache Commons Compress +- DotNetZip Library +- Go Lang project +- lbzip2 project +- pyflate project + +Thanks to Mark Wielaard for assembling the collection. + +--- + +## Related Documentation + +- [Bzip2 Overview](./bzip2.md) — Main documentation +- [Bzip2 Tests](./bzip2-tests.md) — Test suite +- [Compiling](./bzip2-compiling.md) — Build instructions diff --git a/archived/projt-launcher/docs/handbook/bzip2-tests.md b/archived/projt-launcher/docs/handbook/bzip2-tests.md new file mode 100644 index 0000000000..a81fee0449 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/bzip2-tests.md @@ -0,0 +1,80 @@ +# Bzip2 Tests + +> **Directory**: `bzip2/` +> **Test Suites**: Quick (default), Large +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +Bzip2 has two test suites for validating compression and decompression functionality. + +--- + +## Test Suites + +| Suite | Files | Speed | Valgrind | +|-------|-------|-------|----------| +| **Quick** | Small set | Fast (~seconds) | ✅ Enabled | +| **Large** | Many files | Slow (~minutes) | ⌠Disabled | + +--- + +## Quick Test Suite + +The default test suite validates basic functionality: + +### What It Tests + +1. **Compression** — Compress reference files, decompress, verify match +2. **Decompression** — Decompress `.bz2` files, verify against reference +3. **Multiple modes** — Tests various compression levels + +### Running + +```bash +# CMake +cd bzip2/build +ctest -V + +# Meson +meson test -C builddir --print-errorlogs +``` + +--- + +## Large Test Suite + +Comprehensive test with files from various sources: + +### Contents + +- ✅ Good `.bz2` files — Should decompress correctly +- ⌠Bad `.bz2.bad` files — Should fail gracefully + +### Running + +Integrated with the build system test commands. + +--- + +## Valgrind Integration + +The quick tests run under Valgrind if available: + +```bash +# Check if Valgrind is detected +cmake .. -DCMAKE_BUILD_TYPE=Debug +# Valgrind runs automatically during ctest +``` + +The large tests have Valgrind disabled (would take 35+ minutes). + +--- + +## Related Documentation + +- [Bzip2 Overview](./bzip2.md) — Main documentation +- [Compiling](./bzip2-compiling.md) — Build instructions +- [Test Files](./bzip2-testfiles.md) — Test file collection diff --git a/archived/projt-launcher/docs/handbook/bzip2.md b/archived/projt-launcher/docs/handbook/bzip2.md new file mode 100644 index 0000000000..3fe7bb0536 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/bzip2.md @@ -0,0 +1,124 @@ +# Bzip2 `bzip2/` + +> **Type**: Compression Library +> **License**: bzip2 License (BSD-like) +> **Fork Origin**: [GitLab](https://gitlab.com/bzip2/bzip2) | [Sourceware](https://sourceware.org/bzip2/) +> **Status**: Detached Fork (independently maintained) +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +Bzip2/libbz2 is a program and library for lossless, block-sorting data compression. It typically compresses files to within 10% to 15% of the best available techniques (PPM family), while being around twice as fast at compression and six times faster at decompression. + +This repository contains a maintained fork that now advances in its own tree while staying compatible with upstream. + +--- + +## Usage in ProjT Launcher + +Bzip2 is used for: + +- **Mod archive extraction** — Some modpacks use bzip2 compression +- **Legacy support** — Older Minecraft assets may use bzip2 +- **Download optimization** — Efficient compression for large files + +--- + +## Documentation + +| Resource | Description | +|----------|-------------| +| [Compiling Guide](./bzip2-compiling.md) | Build instructions (Meson & CMake) | +| [Test Suite](./bzip2-tests.md) | Quick and large test suites | +| [Test Files](./bzip2-testfiles.md) | Test file collection | +| [Original Manual](../../bzip2/bzip2.txt) | Upstream plain-text manual | +| `bzip2/manual.html` | Full API documentation | + +--- + +## Build Systems + +Bzip2 supports two build systems: + +| Build System | Platform Support | Recommended For | +|--------------|------------------|-----------------| +| **Meson** | Unix, Windows | Unix-like systems | +| **CMake** | Unix, Windows | Cross-platform builds | + +For detailed build instructions, see [bzip2-compiling.md](./bzip2-compiling.md). + +### Quick Build (CMake) + +```bash +cd bzip2 +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . +ctest -V +``` + +--- + +## Contributing + +The bzip2 development happens in the ProjT Launcher repository: + +| Branch | Purpose | +|--------|---------| +| `develop` | Main development branch | +| `release-*` | Stable release branches | +| Feature branches | Experimental work (may be rebased) | + +### Report Issues + +Please report bugs via [GitHub Issues](https://github.com/Project-Tick/ProjT-Launcher/issues). + +--- + +## âš ï¸ Important Notices + +### Warning + +This program and library compresses data by performing several non-trivial transformations. Unless you are 100% familiar with *all* the algorithms contained herein, you should **NOT** modify the compression or decompression machinery. Incorrect changes can lead to disastrous loss of data. + +### Disclaimer + +**NO RESPONSIBILITY IS TAKEN FOR ANY LOSS OF DATA ARISING FROM THE USE OF THIS PROGRAM/LIBRARY.** + +The complexity of the algorithms makes it impossible to rule out the possibility of bugs. Do not compress critical data without backups. + +### Patents + +To the best of our knowledge, bzip2/libbz2 does not use any patented algorithms. However, no patent search has been conducted, so no guarantee can be given. + +--- + +## Copyright & Licensing + +``` +Copyright (C) 1996-2010 Julian Seward +Copyright (C) 2019-2020 Federico Mena Quintero +Copyright (C) 2021-2025 Micah Snyder +Copyright (C) 2025 YongDo-Hyun +Copyright (C) 2025 grxtor +``` + +Licensed under the bzip2 license (BSD-like). See `bzip2/COPYING` for full text. + +--- + +## Related Documentation + +- [zlib](./zlib.md) — Alternative compression library +- [QuaZip](./quazip.md) — ZIP archive wrapper +- [Third-party Libraries](./third-party.md) — All external dependencies + +--- + +## External Links + +- [Bzip2 GitLab](https://gitlab.com/bzip2/bzip2) +- [Bzip2 Sourceware](https://sourceware.org/bzip2/) +- [Wikipedia: Bzip2](https://en.wikipedia.org/wiki/Bzip2) diff --git a/archived/projt-launcher/docs/handbook/ci_support.md b/archived/projt-launcher/docs/handbook/ci_support.md new file mode 100644 index 0000000000..8853645cd2 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/ci_support.md @@ -0,0 +1,103 @@ +# CI Support `ci/` + +> **Location**: `ci/` +> **Purpose**: CI configuration and support files +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +The `ci/` directory contains configuration files, scripts, and Nix expressions that support the CI/CD pipeline. These files are used by GitHub Actions workflows for code quality, validation, and build processes. + +--- + +## Directory Structure + +``` +ci/ +├── code-quality.nix # Nix expression for code quality tools +├── code-quality.sh # Shell script for linting/formatting +├── default.nix # Default Nix entry point +├── parse.nix # Nix parsing utilities +├── pinned.json # Pinned Nix package versions +├── supportedBranches.js # Branch configuration +├── supportedSystems.json # Target system matrix +├── supportedVersions.nix # Version compatibility +├── update-pinned.sh # Update pinned packages +├── OWNERS # Code ownership file +│ +├── codeowners-validator/ # CODEOWNERS validation +├── eval/ # Evaluation helpers +├── github-script/ # GitHub Script automation +└── nixpkgs-vet/ # Nixpkgs validation +``` + +--- + +## File Descriptions + +### Core Files + +| File | Description | +|------|-------------| +| `code-quality.nix` | Nix expression defining code quality tool environment | +| `code-quality.sh` | Main script for running clang-format, linters, etc. | +| `default.nix` | Default Nix derivation entry point | + +### Configuration + +| File | Description | +|------|-------------| +| `pinned.json` | Pinned nixpkgs revision for reproducibility | +| `supportedSystems.json` | Build matrix (Linux, macOS, Windows) | +| `supportedVersions.nix` | Supported version compatibility matrix | +| `supportedBranches.js` | CI branch filtering rules | + +### Subdirectories + +| Directory | Purpose | +|-----------|---------| +| `codeowners-validator/` | Validates CODEOWNERS file format | +| `eval/` | Nix evaluation helpers for project validation | +| `github-script/` | JavaScript helpers for GitHub Actions | +| `nixpkgs-vet/` | Nixpkgs best practices validation | + +--- + +## Usage + +### Running Code Quality Checks + +```bash +# Using Nix +nix-shell ci/default.nix --run "./ci/code-quality.sh" + +# Or directly +./ci/code-quality.sh +``` + +### Updating Pinned Packages + +```bash +./ci/update-pinned.sh +``` + +--- + +## Integration + +These files are used by: + +- `ci-lint.yml` — Code quality workflow +- `ci-new.yml` — Main orchestrator +- `flake.nix` — Nix flake development shell + +--- + +## Related Documentation + +- [Workflows](./workflows.md) — GitHub Actions overview +- [CI Evaluation](./ptcieval.md) — Project validation +- [GitHub Scripts](./ptcigh.md) — Automation helpers +- [Nix Packaging](./nix.md) — Nix build system diff --git a/archived/projt-launcher/docs/handbook/cmark.md b/archived/projt-launcher/docs/handbook/cmark.md new file mode 100644 index 0000000000..7f9e1cf33e --- /dev/null +++ b/archived/projt-launcher/docs/handbook/cmark.md @@ -0,0 +1,179 @@ +# cmark `cmark/` + +> **Type**: Markdown Parser Library +> **License**: BSD-2-Clause (+ MIT for some components) +> **Fork Origin**: [GitHub](https://github.com/commonmark/cmark) +> **Status**: Detached Fork (independently maintained) +> **Specification**: [CommonMark](https://commonmark.org/) +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +`cmark` is the C reference implementation of [CommonMark](https://commonmark.org/), a strongly specified, highly compatible version of Markdown. It provides both a shared library (`libcmark`) and a standalone program for parsing and rendering CommonMark documents. + +ProjT Launcher maintains a fork of cmark for controlled integration, CI validation, and monorepo compatibility. + +--- + +## Usage in ProjT Launcher + +cmark is used for: + +- **Mod descriptions** — Rendering mod README files +- **News display** — Formatting launcher news and announcements +- **Instance notes** — User-created markdown notes +- **Wiki integration** — Processing documentation + +--- + +## Features + +| Feature | Status | +|---------|--------| +| CommonMark 0.31 compliant | ✅ | +| Streaming/iterating parser | ✅ | +| Safe HTML rendering | ✅ | +| UTF-8 support | ✅ | +| Multiple output formats | ✅ | +| Extensible architecture | ✅ | + +### Output Formats + +- HTML +- Groff man pages +- CommonMark (normalized) +- XML +- LaTeX + +--- + +## Build Instructions + +### Prerequisites + +- **CMake 3.12+** +- **C99 compiler** (GCC, Clang, MSVC) +- **Python 3.6+** (for tests) + +### Quick Build + +```bash +cd cmark +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . +ctest -V +``` + +### Build Options + +| Option | Description | Default | +|--------|-------------|---------| +| `CMARK_TESTS` | Build test suite | `ON` | +| `CMARK_SHARED` | Build shared library | `ON` | +| `CMARK_STATIC` | Build static library | `OFF` | +| `CMARK_LIB_FUZZER` | Build libFuzzer harness | `OFF` | + +--- + +## API Usage + +### Basic Example + +```c +#include +#include +#include +#include + +int main() { + const char *markdown = "# Hello World\n\nThis is **CommonMark**!"; + char *html = cmark_markdown_to_html(markdown, strlen(markdown), CMARK_OPT_DEFAULT); + printf("%s", html); + free(html); + return 0; +} +``` + +### Streaming Parser + +```c +#include + +cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT); +cmark_parser_feed(parser, chunk1, strlen(chunk1)); +cmark_parser_feed(parser, chunk2, strlen(chunk2)); +cmark_node *document = cmark_parser_finish(parser); + +char *html = cmark_render_html(document, CMARK_OPT_DEFAULT); + +cmark_node_free(document); +cmark_parser_free(parser); +free(html); +``` + +--- + +## Safety Features + +cmark is designed to be safe by default: + +- ✅ **No buffer overflows** — Extensively fuzzed +- ✅ **HTML sanitization** — Safe mode available +- ✅ **Memory safety** — No use-after-free bugs +- ✅ **Predictable output** — Spec-compliant behavior + +### Safe Rendering Options + +```c +// Use CMARK_OPT_SAFE to prevent raw HTML passthrough +char *html = cmark_markdown_to_html(md, len, CMARK_OPT_SAFE); +``` + +--- + +## Documentation + +| Resource | Location | +|----------|----------| +| API Reference | `cmark/src/cmark.h` | +| Man Page | `cmark/man/man3/cmark.3` | +| [Changelog](../../cmark/changelog.txt) | Upstream change history | +| CommonMark Spec | [spec.commonmark.org](https://spec.commonmark.org/) | + +--- + +## Copyright & Licensing + +``` +Copyright (c) 2014, John MacFarlane + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice. +2. Redistributions in binary form must reproduce the above copyright notice. + +THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY. +``` + +Full license: `cmark/COPYING` + +--- + +## Related Documentation + +- [toml++](./tomlplusplus.md) — TOML parser (similar use case) +- [libnbt++](./libnbtplusplus.md) — NBT format parser +- [Third-party Libraries](./third-party.md) — All dependencies + +--- + +## External Links + +- [cmark GitHub](https://github.com/commonmark/cmark) +- [CommonMark Specification](https://commonmark.org/) +- [CommonMark Dingus](https://spec.commonmark.org/dingus/) — Online tester +- [cmark-gfm](https://github.com/github/cmark-gfm) — GitHub's extended fork diff --git a/archived/projt-launcher/docs/handbook/extra-cmake-modules.md b/archived/projt-launcher/docs/handbook/extra-cmake-modules.md new file mode 100644 index 0000000000..43e175058d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/extra-cmake-modules.md @@ -0,0 +1,67 @@ +# Extra CMake Modules `extra-cmake-modules/` + +> **Type**: CMake Module Collection +> **License**: BSD-3-Clause +> **Fork Origin**: [KDE ECM](https://api.kde.org/ecm/) +> **Status**: Detached Fork (independently maintained) +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +Extra CMake Modules (ECM) provides additional CMake modules beyond what CMake ships by default. This includes: + +- `find_package()` modules for common software +- Utility modules for common CMake tasks +- Toolchain files for cross-compilation +- Common build settings used by KDE software + +While ECM originates from the KDE project, it is useful for any CMake-based project. + +--- + +## Usage in ProjT Launcher + +ECM is used for: + +- **Qt integration** - Finding Qt components +- **Build utilities** - Common CMake patterns +- **Cross-platform support** - Toolchain configurations + +--- + +## Key Modules + +| Module | Purpose | +|--------|---------| +| `ECMQueryQt` | Query Qt installation details | +| `ECMInstallIcons` | Install icon themes | +| `ECMGenerateHeaders` | Generate forwarding headers | +| `ECMSetupVersion` | Configure project versioning | + +--- + +## Build Integration + +ECM is included as a subdirectory: + +```cmake +add_subdirectory(extra-cmake-modules) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/extra-cmake-modules/modules") +``` + +--- + +## Documentation + +- [ECM API Documentation](https://api.kde.org/ecm/) +- [KDE Community Wiki](https://community.kde.org/Policies/CMake_Coding_Style) +- [Original README](../../extra-cmake-modules/README.md) — Upstream library documentation + +--- + +## Related Documentation + +- [Third-party Libraries](./third-party.md) +- [Workflows](./workflows.md) diff --git a/archived/projt-launcher/docs/handbook/help-pages.md b/archived/projt-launcher/docs/handbook/help-pages.md new file mode 100644 index 0000000000..e263f39fac --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages.md @@ -0,0 +1,37 @@ +# ProjT Launcher Help Pages + +> **Player docs**: `docs/handbook/help-pages/` (copied from `website/projtlauncher/wiki/help-pages/`) +> **Assets**: `website/projtlauncher/img/screenshots/` (`/projtlauncher/img/screenshots/...`) +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +User-facing help articles for ProjT Launcher cover launcher settings, platform importers, and common instance workflows. The canonical copies now live in the handbook under `docs/handbook/help-pages/` so they can be built alongside the rest of the documentation. Content originated from the upstream Prism Launcher wiki but has been rebranded and rewired to local screenshot paths. + +--- + +## Content Map + +- **Settings**: `apis.md`, `launcher-settings.md`, `java-settings.md`, `java-wizard.md`, `language-settings.md`, `minecraft-settings.md`, `proxy-settings.md`, `notes.md`. +- **Instances & worlds**: `instance-copy.md`, `instance-version.md`, `worlds.md`, `screenshots-management.md`, `zip-import.md`. +- **Platforms/importers**: `vanilla-platform.md`, `mod-platform.md`, `modrinth-platform.md`, `ftb-platform.md`, `technic-platform.md`, `atl-platform.md`, `flame-platform.md`. +- **Mods/loaders**: `loader-mods.md` for selecting Fabric/Quilt/Forge/NeoForge components. +- **Automation & tools**: `custom-commands.md`, `environment-variables.md`, `tools.md`, and API key setup in `apis.md`. + +--- + +## Assets & Linking + +- Screenshots live in `website/projtlauncher/img/screenshots/` and are referenced as `/projtlauncher/img/screenshots/.png`. +- Cross-page links target sibling handbook pages or existing getting-started docs (for example `../getting-started/installing-java/`). +- Several articles include `` placeholders for missing screenshots or descriptions (notably APIs, Launcher Settings, Minecraft Settings, Proxy Settings, Tools, Instance Version). + +--- + +## Editing / Porting Notes + +- Handbook copies do not use frontmatter; the first heading is injected if missing. +- Add or update screenshots in `website/projtlauncher/img/screenshots/` and keep paths consistent (`/projtlauncher/img/screenshots/.png`). +- If updating the upstream wiki versions in `website/projtlauncher/wiki/help-pages/`, mirror the edits into `docs/handbook/help-pages/` so handbook builds stay in sync. diff --git a/archived/projt-launcher/docs/handbook/help-pages/apis.md b/archived/projt-launcher/docs/handbook/help-pages/apis.md new file mode 100644 index 0000000000..605807190c --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/apis.md @@ -0,0 +1,43 @@ +# APIs + +## Services + +![Services tab under APIs tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_services.png) + +### Pastebin Service + + + +This is where you can choose which Paste Service to use. + +### Metadata Server + + + +This is where you can set where the launcher gets it metadata. + +## API Keys + +![User API Keys tab under APIs tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_services.png) + +### Microsoft Authentication + + + +Set this if you want to use your own client ID for Microsoft Authentication. + +### CurseForge Core API + + + +Set this if you want to use your own CurseForge API key. + +## Miscellaneous + +![Miscellaneous tab under APIs tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_services.png) + +### User Agent + + + +Set this if you want to use a custom User Agent. diff --git a/archived/projt-launcher/docs/handbook/help-pages/atl-platform.md b/archived/projt-launcher/docs/handbook/help-pages/atl-platform.md new file mode 100644 index 0000000000..2dbf0f1aa7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/atl-platform.md @@ -0,0 +1,5 @@ +# ATLauncher + +![Launcher tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_atlauncher.png) + +Here you can browse ATLauncher packs and install them. diff --git a/archived/projt-launcher/docs/handbook/help-pages/custom-commands.md b/archived/projt-launcher/docs/handbook/help-pages/custom-commands.md new file mode 100644 index 0000000000..ad53937859 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/custom-commands.md @@ -0,0 +1,16 @@ +# Custom Commands + +![Custom Commands tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_general.png) + +Pre-launch command runs before the instance launches and post-exit command runs after it exits. + +Both will be run in the launcher's working folder with extra environment variables: + +- $INST_NAME - Name of the instance +- $INST_ID - ID of the instance (its folder name) +- $INST_DIR - absolute path of the instance +- $INST_MC_DIR - absolute path of Minecraft +- $INST_JAVA - Java binary used for launch +- $INST_JAVA_ARGS - command-line parameters used for launch (warning: will not work correctly if arguments contain spaces) + +Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux) diff --git a/archived/projt-launcher/docs/handbook/help-pages/environment-variables.md b/archived/projt-launcher/docs/handbook/help-pages/environment-variables.md new file mode 100644 index 0000000000..736bd4b409 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/environment-variables.md @@ -0,0 +1,5 @@ +# Environment Variables + +![Custom Commands tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_general.png) + +Here you can add environment variables to be provided to the java runtime when launching the game. This can be useful for setting up specific configurations or debugging options. diff --git a/archived/projt-launcher/docs/handbook/help-pages/flame-platform.md b/archived/projt-launcher/docs/handbook/help-pages/flame-platform.md new file mode 100644 index 0000000000..192b9f74fb --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/flame-platform.md @@ -0,0 +1,7 @@ +# CurseForge + +![CurseForge tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_curseforge.png) + +Here you can browse CurseForge packs and install them. + +Note: Some modpacks may require the downloading of certain components via browser. diff --git a/archived/projt-launcher/docs/handbook/help-pages/ftb-platform.md b/archived/projt-launcher/docs/handbook/help-pages/ftb-platform.md new file mode 100644 index 0000000000..33d766946b --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/ftb-platform.md @@ -0,0 +1,7 @@ +# FTB + +![FTB tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance.png) + +Here you can browse FTB packs and install them. + +Note: Some modpacks may require the downloading of certain components via browser. diff --git a/archived/projt-launcher/docs/handbook/help-pages/index.md b/archived/projt-launcher/docs/handbook/help-pages/index.md new file mode 100644 index 0000000000..7f65ed3f35 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/index.md @@ -0,0 +1,39 @@ +# Help Pages + +Guides for ProjT Launcher settings, instance management, and mod platform importers. Content is adapted from the upstream Prism Launcher help pages with ProjT branding and local screenshots. + +## Settings + +- [Launcher Settings](./launcher-settings/) +- [Java Settings](./java-settings/) +- [Java Wizard](./java-wizard/) +- [Language Settings](./language-settings/) +- [Minecraft Settings](./minecraft-settings/) +- [Proxy Settings](./proxy-settings/) +- [Custom Commands](./custom-commands/) +- [Environment Variables](./environment-variables/) +- [APIs](./apis/) +- [External Tools](./tools/) +- [Notes](./notes/) + +## Instances + +- [Instance Version](./instance-version/) +- [Instance Copy](./instance-copy/) +- [Worlds](./worlds/) +- [Screenshots](./screenshots-management/) +- [Zip Import](./zip-import/) + +## Mods & Downloads + +- [Mods](./loader-mods/) +- [Mod Downloader](./mod-platform/) + +## Modpack Platforms + +- [Custom / Vanilla](./vanilla-platform/) +- [Modrinth](./modrinth-platform/) +- [CurseForge](./flame-platform/) +- [FTB](./ftb-platform/) +- [Technic](./technic-platform/) +- [ATLauncher](./atl-platform/) diff --git a/archived/projt-launcher/docs/handbook/help-pages/instance-copy.md b/archived/projt-launcher/docs/handbook/help-pages/instance-copy.md new file mode 100644 index 0000000000..1e334997e6 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/instance-copy.md @@ -0,0 +1,75 @@ +# Copy an Instance + +When you right click on an instance, you can choose to copy it. After selecting this option, you will see a new window. You can choose an icon, name and a group for the new instance. + +![Copy menu when right click on instance](/projtlauncher/img/screenshots/projtlauncher_dark_create_instance_shortcut.png) + +## Instance Copy Options + +This is where you can find options and copy settings for your new instance. + +### Keep play time + +This option will allow you to keep your playtime from your original instance, if this setting is enabled in Global Settings, see: [Game Time](../minecraft-settings/#game-time) + +### Copy saves + +This option allows you to copy all saved games from your original instance to your new one. + +### Copy game options + +This option allows you to copy all game settings, if you want your configuration to be cloned. + +### Copy resource packs + +This option allows you to copy all the resource packs to your new instance. + +### Copy screenshots + +This option allows you to copy all your screenshots to your new instance folder. + +### Copy shader packs + +This option allows you to copy all your shader packs to your new instance folder. + +### Copy servers + +This option allows you to copy all your servers to your new instance. You can find them in the multiplayer menu of your new instance. + +### Copy mods + +This option allows you to copy all your mods to your new instance folder. + +### Select all + +This option allows you to select all the precedent options. + +## Advanced Copy Options + +This is where you can set more advanced options, such as links or cloning. + +### Symbolic and Hard Link Options + +You can't use these options if your partition is on FAT filesystem. + +#### Use symbolic links + +This option will create a symbolic link between the two instances. + +#### Use hard links + +This option will create a hard link between the two instances. + +#### Link files recursively + +This option will link all files, instead of just the parent folder. + +#### Don't link saves + +This option will disable the link of the saves. The world saves will be copied instead. + +### CoW (Copy-on-Write) Options + +#### Clone instead of copying + +This option is only supported on APFS, BTRFS, BCACHEFS, REFS, XFS and ZFS filesystems. diff --git a/archived/projt-launcher/docs/handbook/help-pages/instance-version.md b/archived/projt-launcher/docs/handbook/help-pages/instance-version.md new file mode 100644 index 0000000000..c6d6f6eacb --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/instance-version.md @@ -0,0 +1,35 @@ +# Instance Version + +![Version tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_version.png) + +This page is for managing the core parts of the Instance. + +### Selection + + + +Here you can change the version, change the load order and remove components. + +### Edit + + + +This is for customizing the .json file that is used to load that component. + +### Install + + + +From here you can install loader mods. + +### Advanced + + + +This is for adding and replacing components that change the Minecraft.jar + +### Folder + + + +This are shortcuts that open a specific folder. diff --git a/archived/projt-launcher/docs/handbook/help-pages/java-settings.md b/archived/projt-launcher/docs/handbook/help-pages/java-settings.md new file mode 100644 index 0000000000..2dd19d74b9 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/java-settings.md @@ -0,0 +1,40 @@ +# Java Settings + +### Java settings + +In this page you can set the global Java settings. + +![Java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +## Memory + +![Memory section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +Java memory settings. +NOTE: MORE RAM ALLOCATED DOESN'T MEAN BETTER PERFORMANCE! In fact, in most use cases (except you're playing big modpacks) 4GB of ram allocated should be more than enough + +## Java Runtime + +![Java Runtime section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +This is where the settings for the Java runtime live, like the location of the runtime and any Java arguments to use. + +For information about how to get a correct Java version, see: [Installing Java](../getting-started/installing-java/). + +**Auto-detect** will check your computer for all java versions and show you a list of them, the best one on top. + +**Test** can be used to test the selected Java runtime along with your memory settings and JVM arguments without starting the game. + +**Skip java compatibility checks** skips java compatibility checks at game launch + +**Autodetect Java version** (Recommended) sets the correct java version at game launch (only looks with managed (see below) java and will change the java path in instance settings) + +**Auto-download Mojang Java** (Recomnended) will automatically download needed the Java runtimes you need (needs the above setting to be on) + +## Java Management + +![Java Management section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +This is where you can download and remove the java that were installed by ProjT Launcher. + +![Java Download section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) diff --git a/archived/projt-launcher/docs/handbook/help-pages/java-wizard.md b/archived/projt-launcher/docs/handbook/help-pages/java-wizard.md new file mode 100644 index 0000000000..76096ee8f1 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/java-wizard.md @@ -0,0 +1,11 @@ +# Java Wizard + +![Java wizard page displayed on the first launch](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +In this page you can set the global Java settings. + +For more information on how to manually download and install Java check the documentation [found here](../getting-started/installing-java/). + +For more information about each Java setting check the documentation [found here](../java-settings/). + +We recommend that new users enable both the Autodetect and Auto-download Java features, in order not to need to manually set up their java path for each instance. diff --git a/archived/projt-launcher/docs/handbook/help-pages/language-settings.md b/archived/projt-launcher/docs/handbook/help-pages/language-settings.md new file mode 100644 index 0000000000..e024a889b6 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/language-settings.md @@ -0,0 +1,5 @@ +# Language Settings + +![Language tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_language.png) + +In this Page, you can select the language ProjT Launcher should use. diff --git a/archived/projt-launcher/docs/handbook/help-pages/launcher-settings.md b/archived/projt-launcher/docs/handbook/help-pages/launcher-settings.md new file mode 100644 index 0000000000..781f71faec --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/launcher-settings.md @@ -0,0 +1,61 @@ +# Launcher Settings + +## Features + +![Features tab under Launcher tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_general.png) + +### Folders + + + +This is where you can set the location for the Instances, Mods & Icons folders. + +### Mods + + + +Disable the usage of metadata provided by Modrinth and CurseForge for mods. + +## User Interface + +![User Interface tab under Launcher tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_appearance.png) + +### Instance view sorting mode + + + +How the instances should be sorted. + +### Theme + + + +This is where you can choose which Icon Theme, Color Theme or Background Cat you want. + +### Tools + + + +This is where you can choose if you want a menubar or toolbar. + +## Console + +![Console tab under Launcher tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_settings.png) + +### Console Settings + + + +This is where you set when the log window should appear. + +### History limit + + + +This is where you set if you want to limit the log length and by how much. + +### Console font + + + +This is where you can change the font and font size of the log. diff --git a/archived/projt-launcher/docs/handbook/help-pages/loader-mods.md b/archived/projt-launcher/docs/handbook/help-pages/loader-mods.md new file mode 100644 index 0000000000..92a4e9dbaa --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/loader-mods.md @@ -0,0 +1,9 @@ +# Mods + +![Mods tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_mods.png) + +This page reflects the contents of the instance's mods folder and is used for mods that loaders like Forge, Fabric, NeoForge, Quilt and LiteLoader can read and add to the game. Unlike an ordinary file explorer, it also shows details about the mods like version and name. + +You can drag and drop more mods into the view, just like any other folder. The mods also have a checkbox next to them, which allows you to temporarily disable them. + +Clicking "Download Mods" you can download mods from Modrinth and CurseForge. diff --git a/archived/projt-launcher/docs/handbook/help-pages/minecraft-settings.md b/archived/projt-launcher/docs/handbook/help-pages/minecraft-settings.md new file mode 100644 index 0000000000..f35a71b51f --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/minecraft-settings.md @@ -0,0 +1,35 @@ +# Minecraft Settings + +![Minecraft tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_minecraft.png) + +### Window Size + + + +This is where you set the Window size and if it should be maximized\*. + +\*Only works on 1.5.2 and older + +### Native library workarounds + + + +Enable the usage of system libraries instead of the included ones. + +### Performance + + + +Enable some external tools related to performance. + +### Game time + + + +Set if you want to record play time and which to show. + +### Miscellaneous + + + +Set when you want to close or quit the launcher. diff --git a/archived/projt-launcher/docs/handbook/help-pages/mod-platform.md b/archived/projt-launcher/docs/handbook/help-pages/mod-platform.md new file mode 100644 index 0000000000..9376e9d7cd --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/mod-platform.md @@ -0,0 +1,15 @@ +# Mod Downloader + +![Mod downloader on mods tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_download_mods.png) + +In this page, you can download mods from Modrinth and CurseForge. + +On the left, you can choose the mod provider. + +Clicking "Select mod for Download" selects the mod to be downloaded. + +Clicking "Filter options" you can choose how strict the filters will be. + +Clicking "Cancel" returns to the previous page. + +Clicking "OK" allows you to see the mods you have decided to download and to download them. diff --git a/archived/projt-launcher/docs/handbook/help-pages/modrinth-platform.md b/archived/projt-launcher/docs/handbook/help-pages/modrinth-platform.md new file mode 100644 index 0000000000..7ecc0e6695 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/modrinth-platform.md @@ -0,0 +1,5 @@ +# Modrinth + +![Modrinth tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_modrinth.png) + +Here you can browse Modrinth packs and install them. diff --git a/archived/projt-launcher/docs/handbook/help-pages/notes.md b/archived/projt-launcher/docs/handbook/help-pages/notes.md new file mode 100644 index 0000000000..3ac837e64d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/notes.md @@ -0,0 +1,5 @@ +# Notes + +![Notes tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_notes.png) + +Here you can write some notes about your instance in plain text. diff --git a/archived/projt-launcher/docs/handbook/help-pages/proxy-settings.md b/archived/projt-launcher/docs/handbook/help-pages/proxy-settings.md new file mode 100644 index 0000000000..410b77560d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/proxy-settings.md @@ -0,0 +1,21 @@ +# Proxy Settings + +![Proxy tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_proxy.png) + +### Type + + + +This is where you choose if and what type of proxy you want to use. + +### Address and Port + + + +This is where you can set the Address and Port of your proxy server. + +### Authentication + + + +This is where you fill in the login details of your proxy. diff --git a/archived/projt-launcher/docs/handbook/help-pages/screenshots-management.md b/archived/projt-launcher/docs/handbook/help-pages/screenshots-management.md new file mode 100644 index 0000000000..0458eed9da --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/screenshots-management.md @@ -0,0 +1,5 @@ +# Screenshots + +![Screenshots tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_screenshots.png) + +Here you can rename, deleted, copy and upload you in game screenshots. diff --git a/archived/projt-launcher/docs/handbook/help-pages/technic-platform.md b/archived/projt-launcher/docs/handbook/help-pages/technic-platform.md new file mode 100644 index 0000000000..e7fa9caea2 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/technic-platform.md @@ -0,0 +1,5 @@ +# Technic + +![Technic tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_technic.png) + +Here you can browse Technic packs and install them. diff --git a/archived/projt-launcher/docs/handbook/help-pages/tools.md b/archived/projt-launcher/docs/handbook/help-pages/tools.md new file mode 100644 index 0000000000..12c1330cdf --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/tools.md @@ -0,0 +1,29 @@ +# External Tools + +![External Tools tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_tools.png) + + + +### JProfiler + + + +This is where you set to path to your JProfiler executable. + +### JVisualVM + + + +This is where you set to path to your JVisualVM executable. + +### MCEdit + + + +This is where you set to path to your MCEdit executable. + +### External Editors + + + +This is where you set to path to your text editor of choice or leave it blank to use the systems default one. diff --git a/archived/projt-launcher/docs/handbook/help-pages/vanilla-platform.md b/archived/projt-launcher/docs/handbook/help-pages/vanilla-platform.md new file mode 100644 index 0000000000..aa5ce61885 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/vanilla-platform.md @@ -0,0 +1,5 @@ +# Custom / Vanilla + +![Custom tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance.png) + +This is where you can choose which minecraft version you want to play with and what mod loader to install. diff --git a/archived/projt-launcher/docs/handbook/help-pages/worlds.md b/archived/projt-launcher/docs/handbook/help-pages/worlds.md new file mode 100644 index 0000000000..cad69f14cc --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/worlds.md @@ -0,0 +1,5 @@ +# Worlds + +![Worlds tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_worlds.png) + +Here you can do some basic management of your worlds that you have in that Instance. diff --git a/archived/projt-launcher/docs/handbook/help-pages/zip-import.md b/archived/projt-launcher/docs/handbook/help-pages/zip-import.md new file mode 100644 index 0000000000..0078179896 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/help-pages/zip-import.md @@ -0,0 +1,5 @@ +# Zip Import + +![Import from zip tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_import.png) + +Here you can import modpacks from a zip or mrpack file either from a website or from your filesystem. diff --git a/archived/projt-launcher/docs/handbook/images.md b/archived/projt-launcher/docs/handbook/images.md new file mode 100644 index 0000000000..24cad7dd46 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/images.md @@ -0,0 +1,66 @@ +# CI Docker Images + +> **Purpose**: Minimal Qt build images for CI testing +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +These are minimal Docker images containing Qt built from source with specific options for CI testing purposes. + +--- + +## Why Custom Images? + +The standard `jurplel/install-qt-action` downloads pre-built Qt binaries, but: + +- ⌠Does not support all build options +- ⌠Cannot test `-qt-zlib` builds +- ⌠Limited configuration options + +Our custom images provide: + +- ✅ Qt built with `-qt-zlib` option +- ✅ Testing `QUAZIP_USE_QT_ZLIB=ON` builds +- ✅ Full control over Qt configuration + +--- + +## Available Images + +| Image | Qt Version | Platform | +|-------|------------|----------| +| `qt-minimal-linux` | Qt 6.x | Ubuntu | +| `qt-minimal-static` | Qt 6.x (static) | Ubuntu | + +--- + +## Usage in CI + +```yaml +jobs: + test: + runs-on: ubuntu-latest + container: ghcr.io/project-tick/qt-minimal-linux:latest + steps: + - uses: actions/checkout@v4 + - run: cmake -B build -DQUAZIP_USE_QT_ZLIB=ON + - run: cmake --build build +``` + +--- + +## Building Images + +```bash +cd ci/images +docker build -t qt-minimal-linux -f Dockerfile.linux . +``` + +--- + +## Related Documentation + +- [Workflows](./workflows.md) — CI overview +- [QuaZip](./quazip.md) — Uses these images for testing diff --git a/archived/projt-launcher/docs/handbook/javacheck.md b/archived/projt-launcher/docs/handbook/javacheck.md new file mode 100644 index 0000000000..bed3279bc3 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/javacheck.md @@ -0,0 +1,86 @@ +# JavaCheck `javacheck/` + +> **Location**: `javacheck/` +> **Language**: Java +> **Purpose**: Java runtime property detection +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +JavaCheck is a minimal Java program that prints system properties. It's used by ProjT Launcher to detect and validate Java installations. + +--- + +## How It Works + +``` +┌─────────────────┠┌─────────────┠┌──────────────┠+│ ProjT Launcher │────▶│ JavaCheck │────▶│ stdout │ +│ (C++) │ │ (Java) │ │ properties │ +└─────────────────┘ └─────────────┘ └──────────────┘ +``` + +### Execution + +```bash +java -jar javacheck.jar java.version java.home os.arch +``` + +### Output + +``` +java.version=17.0.9 +java.home=/usr/lib/jvm/java-17-openjdk +os.arch=amd64 +``` + +--- + +## Properties Checked + +| Property | Description | +|----------|-------------| +| `java.version` | Java version string | +| `java.home` | JRE installation path | +| `os.arch` | System architecture | +| `java.vendor` | JVM vendor | +| `java.vm.name` | JVM implementation | + +--- + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | All properties found | +| 1 | One or more properties null | + +--- + +## Usage in Launcher + +ProjT Launcher uses JavaCheck to: + +1. **Validate Java installations** — Ensure Java is working +2. **Detect version** — Choose correct Java for Minecraft version +3. **Get architecture** — Match 32/64-bit requirements +4. **Auto-detect Java** — Find installed JREs/JDKs + +--- + +## Building + +```bash +cd javacheck +javac JavaCheck.java +jar cfe javacheck.jar JavaCheck JavaCheck.class +``` + +--- + +## Related Documentation + +- [LauncherJava](./launcherjava.md) — Launch component +- [Program Info](./program_info.md) — Application config diff --git a/archived/projt-launcher/docs/handbook/launcherjava.md b/archived/projt-launcher/docs/handbook/launcherjava.md new file mode 100644 index 0000000000..0ce795e4b8 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/launcherjava.md @@ -0,0 +1,142 @@ +# LauncherJava `launcherjava/` + +> **Location**: `launcherjava/` +> **Language**: Java +> **Purpose**: Minecraft launching component +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +LauncherJava is the Java-based component responsible for actually launching Minecraft. It acts as an intermediary between the C++ launcher and the Minecraft JVM process. + +--- + +## How It Works + +``` +┌─────────────┠┌──────────────┠┌─────────────┠+│ ProjT │────▶│ LauncherJava │────▶│ Minecraft │ +│ Launcher │ │ (Java) │ │ JVM │ +│ (C++) │ │ │ │ │ +└─────────────┘ └──────────────┘ └─────────────┘ + │ │ + │ stdin │ exec + │ (script) │ (launch) + â–¼ â–¼ +``` + +### Launch Sequence + +1. C++ launcher sends launch script via stdin +2. LauncherJava parses the script +3. Waits for `launcher` command +4. Executes Minecraft with configured parameters + +--- + +## Launch Script Format + +The launcher communicates via a text-based protocol: + +```text +mod legacyjavafixer-1.0 +mainClass net.minecraft.launchwrapper.Launch +param --username +param PlayerName +param --version +param ProjT Launcher +param --gameDir +param /path/to/minecraft +param --assetsDir +param /path/to/assets +param --assetIndex +param 1.20.4 +param --uuid +param xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +param --accessToken +param [TOKEN] +param --userType +param msa +windowTitle ProjT Launcher: Instance Name +windowParams 854x480 +userName PlayerName +sessionId token:[TOKEN]:[UUID] +launcher standard +``` + +### Commands + +| Command | Description | +|---------|-------------| +| `mod ` | Load a mod (tweaker) | +| `mainClass ` | Set main class | +| `param ` | Add launch parameter | +| `windowTitle ` | Set window title | +| `windowParams <WxH>` | Set window size | +| `userName <name>` | Player username | +| `sessionId <id>` | Session identifier | +| `launcher <type>` | Execute launch | +| `abort` | Cancel launch | + +--- + +## Launcher Types + +| Type | Use Case | +|------|----------| +| `standard` | All Minecraft versions (recommended) | +| `legacy` | Minecraft < 1.6 (deprecated) | + +### Differences + +- **Standard**: Universal launcher, works with all versions +- **Legacy**: Supports custom window icon/title but only for old versions + +--- + +## Building + +LauncherJava is built as part of the main CMake build: + +```bash +# Built automatically with main project +cmake --build build + +# Or build separately +cd launcherjava +./gradlew build +``` + +--- + +## Debugging + +### Enable Debug Output + +```text +debug on +mainClass ... +``` + +### Attach Profiler + +Since LauncherJava waits for the `launcher` command, you can: + +1. Start LauncherJava +2. Attach your profiler (VisualVM, JProfiler, etc.) +3. Send the `launcher` command + +--- + +## License + +Available under **GPL-3.0** (with classpath exception), sublicensed from its original **Apache-2.0** codebase. + +--- + +## Related Documentation + +- [Program Info](./program_info.md) — Application identity +- [JavaCheck](./javacheck.md) — Java detection diff --git a/archived/projt-launcher/docs/handbook/libnbtplusplus.md b/archived/projt-launcher/docs/handbook/libnbtplusplus.md new file mode 100644 index 0000000000..e07ace9374 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/libnbtplusplus.md @@ -0,0 +1,188 @@ +# libnbt++ `libnbtplusplus/` + +> **Type**: NBT Format Library +> **License**: LGPL-3.0 +> **Fork Origin**: [GitHub](https://github.com/PrismLauncher/libnbtplusplus) +> **Status**: Detached Fork (independently maintained) +> **Format**: Minecraft Named Binary Tag +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +libnbt++ is a free C++ library for Minecraft's Named Binary Tag (NBT) file format. It can read and write compressed and uncompressed NBT files and provides a code interface for working with NBT data. + +### Version History + +- **libnbt++ 1** — Original library (deprecated) +- **libnbt++ 2** — Rewritten with cleaner API (deprecated) +- **libnbt++ 3** — Current version used by ProjT Launcher + +--- + +## Usage in ProjT Launcher + +libnbt++ is used for: + +- **World data** — Reading level.dat and player data +- **Server data** — Parsing server.dat files +- **Mod integration** — Processing NBT-based configurations +- **Backup/export** — Instance data serialization + +--- + +## Features + +| Feature | Status | +|---------|--------| +| NBT reading | ✅ | +| NBT writing | ✅ | +| GZip compression | ✅ | +| Zlib compression | ✅ | +| Big-endian support | ✅ | +| Little-endian (Bedrock) | âš ï¸ Limited | + +### Supported Tag Types + +| Type ID | Tag Name | C++ Type | +|---------|----------|----------| +| 0 | TAG_End | — | +| 1 | TAG_Byte | `int8_t` | +| 2 | TAG_Short | `int16_t` | +| 3 | TAG_Int | `int32_t` | +| 4 | TAG_Long | `int64_t` | +| 5 | TAG_Float | `float` | +| 6 | TAG_Double | `double` | +| 7 | TAG_Byte_Array | `std::vector<int8_t>` | +| 8 | TAG_String | `std::string` | +| 9 | TAG_List | `nbt::tag_list` | +| 10 | TAG_Compound | `nbt::tag_compound` | +| 11 | TAG_Int_Array | `std::vector<int32_t>` | +| 12 | TAG_Long_Array | `std::vector<int64_t>` | + +--- + +## Build Instructions + +### Prerequisites + +- C++11 compatible compiler +- CMake 3.15 or later +- ZLIB (optional, for compression support) + +### Build Steps + +```bash +cd libnbtplusplus +mkdir build && cd build +cmake .. +cmake --build . +``` + +### CMake Options + +| Option | Description | Default | +|--------|-------------|---------| +| `NBT_BUILD_SHARED` | Build shared library | `OFF` | +| `NBT_USE_ZLIB` | Enable zlib support | `ON` | +| `NBT_BUILD_TESTS` | Build tests | `ON` | + +--- + +## API Usage + +### Reading NBT + +```cpp +#include <nbt_tags.h> +#include <io/stream_reader.h> +#include <fstream> + +std::ifstream file("level.dat", std::ios::binary); +nbt::io::stream_reader reader(file, endian::big); + +// Read compound tag +auto result = reader.read_compound(); +nbt::tag_compound& root = result.first; +std::string name = result.second; + +// Access data +auto& level = root["Data"].as<nbt::tag_compound>(); +std::string levelName = level["LevelName"].as<nbt::tag_string>(); +int32_t gameType = level["GameType"].as<nbt::tag_int>(); +``` + +### Writing NBT + +{% raw %} +```cpp +#include <nbt_tags.h> +#include <io/stream_writer.h> +#include <fstream> + +nbt::tag_compound data; +data["LevelName"] = nbt::tag_string("My World"); +data["GameType"] = nbt::tag_int(0); // Survival + +std::ofstream file("level.dat", std::ios::binary); +nbt::io::stream_writer writer(file, endian::big); +writer.write_tag("", nbt::tag_compound{{"Data", std::move(data)}}); +``` +{% endraw %} + +### Working with Lists + +```cpp +// Create a list of strings +nbt::tag_list items(nbt::tag_type::String); +items.push_back(nbt::tag_string("item1")); +items.push_back(nbt::tag_string("item2")); + +// Access list +for (const auto& item : root["Items"].as<nbt::tag_list>()) { + std::cout << item.as<nbt::tag_string>().get() << std::endl; +} +``` + +--- + +## Testing + +```bash +cd libnbtplusplus/build +ctest -V +``` + +--- + +## Documentation + +| Resource | Location | +|----------|----------| +| API Headers | `libnbtplusplus/include/` | +| Tests | `libnbtplusplus/test/` | +| License | `libnbtplusplus/COPYING`, `libnbtplusplus/COPYING.LESSER` | +| Upstream Wiki | [GitHub Wiki](https://github.com/PrismLauncher/libnbtplusplus/wiki) | + +--- + +## Copyright & Licensing + +Full license: `libnbtplusplus/COPYING` + +--- + +## Related Documentation + +- [toml++](./tomlplusplus.md) — TOML configuration parser +- [cmark](./cmark.md) — Markdown parser +- [Third-party Libraries](./third-party.md) — All dependencies + +--- + +## External Links + +- [libnbt++ GitHub](https://github.com/PrismLauncher/libnbtplusplus) +- [NBT Format Wiki](https://minecraft.wiki/w/NBT_format) +- [Minecraft Data Files](https://minecraft.wiki/w/Data_file) diff --git a/archived/projt-launcher/docs/handbook/libqrencode.md b/archived/projt-launcher/docs/handbook/libqrencode.md new file mode 100644 index 0000000000..81682b4aa8 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/libqrencode.md @@ -0,0 +1,221 @@ +# libqrencode `libqrencode/` + +> **Type**: QR Code Generation Library +> **License**: LGPL-2.1-or-later +> **Fork Origin**: [GitHub](https://github.com/fukuchi/libqrencode) +> **Status**: Detached Fork (independently maintained) +> **Standards**: ISO/IEC 18004:2015 +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +libqrencode is a fast and compact library for encoding data into a QR Code symbol. It supports the full range of QR Code functionality as defined by the ISO standard. + +ProjT Launcher maintains a fork of libqrencode for controlled integration and monorepo CI compatibility. + +--- + +## Usage in ProjT Launcher + +libqrencode is used for: + +- **Account linking** — Microsoft account QR authentication +- **Share URLs** — Quick sharing of instance/mod links +- **Export/Import** — Instance configuration sharing + +--- + +## Features + +| Feature | Status | +|---------|--------| +| QR Code Model 1 & 2 | ✅ | +| Micro QR Code | ✅ | +| All error correction levels | ✅ | +| Structured append | ✅ | +| 8-bit byte mode | ✅ | +| Kanji mode | ✅ | +| ECI mode | ✅ | + +### Error Correction Levels + +| Level | Recovery | Use Case | +|-------|----------|----------| +| L | ~7% | High density | +| M | ~15% | Standard (default) | +| Q | ~25% | Industrial | +| H | ~30% | Critical data | + +--- + +## Build Instructions + +### Prerequisites + +- **CMake 3.10+** or **Autotools** +- **C99 compiler** (GCC, Clang, MSVC) +- **libpng** (optional, for PNG output) + +### CMake Build + +```bash +cd libqrencode +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . +ctest -V +``` + +### Autotools Build + +```bash +cd libqrencode +./autogen.sh +./configure +make +make check +``` + +### CMake Options + +| Option | Description | Default | +|--------|-------------|---------| +| `WITH_TOOLS` | Build qrencode CLI tool | `ON` | +| `WITH_PNG` | Enable PNG support | `ON` | +| `WITH_TESTS` | Build test suite | `ON` | +| `BUILD_SHARED_LIBS` | Build shared library | `ON` | + +--- + +## API Usage + +### Basic Example + +```c +#include <qrencode.h> +#include <stdio.h> + +int main() { + QRcode *qr = QRcode_encodeString("https://projecttick.org", + 0, // Version (0 = auto) + QR_ECLEVEL_M, // Error correction + QR_MODE_8, // Encoding mode + 1); // Case sensitive + + if (qr == NULL) { + fprintf(stderr, "Failed to encode\n"); + return 1; + } + + // Access the QR code data + printf("Version: %d\n", qr->version); + printf("Width: %d\n", qr->width); + + // qr->data contains the QR code matrix + // Each byte: bit 0 = module (1=black, 0=white) + + QRcode_free(qr); + return 0; +} +``` + +### Structured Append (Large Data) + +```c +// Split data across multiple QR codes +QRcode_List *list = QRcode_encodeStringStructured( + large_data, 0, QR_ECLEVEL_M, QR_MODE_8, 1); + +for (QRcode_List *entry = list; entry != NULL; entry = entry->next) { + // Process each QR code in sequence + process_qrcode(entry->code); +} + +QRcode_List_free(list); +``` + +--- + +## Command Line Tool + +The `qrencode` CLI tool creates QR code images: + +```bash +# Generate PNG +qrencode -o output.png "Hello World" + +# Generate SVG +qrencode -t SVG -o output.svg "https://projecttick.org" + +# Generate ASCII art +qrencode -t ASCII "Hello World" + +# Specify error correction level +qrencode -l H -o secure.png "Sensitive Data" +``` + +### Output Formats + +- PNG (requires libpng) +- EPS +- SVG +- ANSI terminal +- ASCII art +- UTF-8 terminal + +--- + +## Documentation + +| Resource | Location | +|----------|----------| +| API Reference | `libqrencode/qrencode.h` | +| Man Page | `libqrencode/qrencode.1.in` | +| [Original README](../../libqrencode/README.md) | Upstream library documentation | + +--- + +## Testing + +```bash +cd libqrencode/build +ctest -V + +# Or with autotools +cd libqrencode +make check +``` + +See [ci-libqrencode.yml](../../.github/workflows/ci-libqrencode.yml) for CI configuration. + +--- + +## Copyright & Licensing + +``` +Copyright (C) 2006-2017 Kentaro Fukuchi <kentaro@fukuchi.org> + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. +``` + +Full license: `libqrencode/COPYING` + +--- + +## Related Documentation + +- [Third-party Libraries](./third-party.md) — All dependencies + +--- + +## External Links + +- [libqrencode GitHub](https://github.com/fukuchi/libqrencode) +- [ISO/IEC 18004](https://www.iso.org/standard/62021.html) — QR Code standard +- [QR Code Wikipedia](https://en.wikipedia.org/wiki/QR_code) +- [ZXing](https://github.com/zxing/zxing) — QR Code reading library diff --git a/archived/projt-launcher/docs/handbook/linux-packaging.md b/archived/projt-launcher/docs/handbook/linux-packaging.md new file mode 100644 index 0000000000..728034683f --- /dev/null +++ b/archived/projt-launcher/docs/handbook/linux-packaging.md @@ -0,0 +1,121 @@ +# Linux Packaging + +> **Location**: `docs/packaging/os-specific/linux/` +> **Platforms**: Nix, Flatpak, distribution packages +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +ProjT Launcher supports multiple Linux packaging formats to reach different user bases and distribution requirements. + +--- + +## Packaging Formats + +### Nix / NixOS + +The recommended method for reproducible builds. + +**Location**: `nix/`, `flake.nix` + +```sh +# Build +nix build .#projtlauncher + +# Run without installing +nix run .#projtlauncher + +# Development shell +nix develop +``` + +See [nix.md](./nix.md) for detailed instructions. + +### Flatpak + +Sandboxed application format. + +**Location**: `docs/packaging/os-specific/linux/flathub/` + +The Flatpak manifest is maintained separately for Flathub submission. + +### Distribution Packages + +ProjT Launcher is packaged for various distributions: + +| Distribution | Package | Status | +|--------------|---------|--------| +| Arch Linux (AUR) | `projtlauncher` | Community | +| Fedora COPR | `projtlauncher` | Community | +| openSUSE OBS | `projtlauncher` | Community | + +Check [Repology](https://repology.org/project/projtlauncher/versions) for current availability. + +--- + +## Building for Distribution + +### Requirements + +- CMake 3.22+ +- Qt 6.x +- C++20 compiler +- Ninja (recommended) + +### Build Commands + +```sh +cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DLauncher_BUILD_PLATFORM=linux +cmake --build build +DESTDIR="$pkgdir" cmake --install build +``` + +### Platform Identifier + +Set `Launcher_BUILD_PLATFORM` to identify your distribution: + +```cmake +-DLauncher_BUILD_PLATFORM=archlinux +-DLauncher_BUILD_PLATFORM=fedora +-DLauncher_BUILD_PLATFORM=flatpak +``` + +--- + +## Desktop Integration + +### Desktop File + +Installed to `/usr/share/applications/`: + +``` +org.projecttick.ProjTLauncher.desktop +``` + +### Icon + +Installed to `/usr/share/icons/hicolor/`: + +``` +org.projecttick.ProjTLauncher.svg +``` + +### AppStream Metadata + +Installed to `/usr/share/metainfo/`: + +``` +org.projecttick.ProjTLauncher.metainfo.xml +``` + +--- + +## Related Documentation + +- [Nix Packaging](./nix.md) +- [Program Info](./program_info.md) - Branding configuration diff --git a/archived/projt-launcher/docs/handbook/nix.md b/archived/projt-launcher/docs/handbook/nix.md new file mode 100644 index 0000000000..5223b582e5 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/nix.md @@ -0,0 +1,212 @@ +# Nix Packaging `nix/` + +> **Location**: `nix/`, `flake.nix`, `shell.nix`, `default.nix` +> **Platform**: NixOS, Nix package manager +> **Purpose**: Reproducible builds and packaging +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +ProjT Launcher provides first-class Nix support for reproducible builds, development environments, and packaging. Both Flakes and traditional Nix expressions are supported. + +--- + +## Quick Start + +### Run Without Installing + +```bash +nix run github:Project-Tick/ProjT-Launcher +``` + +### Install via Flakes + +```bash +nix profile install github:Project-Tick/ProjT-Launcher +``` + +### Development Shell + +```bash +# With Flakes +nix develop github:Project-Tick/ProjT-Launcher + +# Traditional +nix-shell +``` + +--- + +## Binary Cache + +We use Cachix for pre-built binaries. Add to avoid rebuilds: + +<!-- ### NixOS Configuration + +```nix +{ + nix.settings = { + trusted-substituters = [ "https://cache.projecttick.org" ]; + trusted-public-keys = [ + "cache.projecttick.org-1:HrpR1buYLhqx0ooS1rMgyHChoYf+faZm82hsIY6JS+s=" + ]; + }; +} +``` --> + +### Flakes (Temporary) + +```bash +nix run github:Project-Tick/ProjT-Launcher --accept-flake-config +``` + +--- + +## Installation Methods + +### Flakes (Recommended) + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + projtlauncher.url = "github:Project-Tick/ProjT-Launcher"; + }; + + outputs = { nixpkgs, projtlauncher, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + ({ pkgs, ... }: { + environment.systemPackages = [ + projtlauncher.packages.${pkgs.system}.projtlauncher + ]; + }) + ]; + }; + }; +} +``` + +### Using Overlay + +```nix +{ + nixpkgs.overlays = [ projtlauncher.overlays.default ]; + environment.systemPackages = [ pkgs.projtlauncher ]; +} +``` + +### Traditional Nix (No Flakes) + +```nix +{ pkgs, ... }: +{ + environment.systemPackages = [ + (import (builtins.fetchTarball + "https://github.com/Project-Tick/ProjT-Launcher/archive/develop.tar.gz" + )).packages.${pkgs.system}.projtlauncher + ]; +} +``` + +--- + +## Package Variants + +| Package | Description | +|---------|-------------| +| `projtlauncher` | Fully wrapped with runtime dependencies | +| `projtlauncher-unwrapped` | Minimal build for customization | + +### Customization Options + +The wrapped package accepts these overrides: + +| Option | Default | Description | +|--------|---------|-------------| +| `additionalLibs` | `[]` | Extra `LD_LIBRARY_PATH` entries | +| `additionalPrograms` | `[]` | Extra `PATH` entries | +| `controllerSupport` | `isLinux` | Game controller support | +| `gamemodeSupport` | `isLinux` | Feral GameMode integration | +| `jdks` | `[jdk21 jdk17 jdk8]` | Available Java runtimes | +| `msaClientID` | `null` | Microsoft Auth client ID | +| `textToSpeechSupport` | `isLinux` | TTS support | + +### Example Override + +```nix +projtlauncher.override { + jdks = [ pkgs.jdk21 pkgs.jdk17 ]; + gamemodeSupport = false; +} +``` + +--- + +## Development + +### Enter Development Shell + +```bash +nix develop +# or +nix-shell +``` + +### Build Locally + +```bash +nix build .#projtlauncher +./result/bin/projtlauncher +``` + +--- + +## File Structure + +``` +├── flake.nix # Flake definition +├── flake.lock # Locked dependencies +├── default.nix # Flake-compat entry +├── shell.nix # Development shell +└── nix/ + ├── default.nix # Package derivation + └── ... +``` + +--- + +## Troubleshooting + +### Binary Cache Not Working + +Ensure the cache is in `trusted-substituters` (requires root): + +```bash +sudo nix-channel --update +``` + +### Build Failures + +Try with fresh nixpkgs: + +```bash +nix build --override-input nixpkgs github:NixOS/nixpkgs/nixos-unstable +``` + +--- + +## Related Documentation + +- [CI Support](./ci_support.md) — CI Nix integration +- [CI Evaluation](./ptcieval.md) — Nix-based validation + +--- + +## External Links + +- [Nix Manual](https://nixos.org/manual/nix/stable/) +- [NixOS Wiki: ProjT Launcher](https://wiki.nixos.org/wiki/ProjT_Launcher) +- [Cachix](https://cachix.org/) diff --git a/archived/projt-launcher/docs/handbook/program_info.md b/archived/projt-launcher/docs/handbook/program_info.md new file mode 100644 index 0000000000..4a861ef0c1 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/program_info.md @@ -0,0 +1,65 @@ +# Program Info `program_info/` + +> **Location**: `program_info/` +> **Purpose**: Application branding and configuration +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +The `program_info/` directory contains ProjT Launcher's program information, including application identity, branding assets, and configuration endpoints. + +--- + +## Contents + +### Application Identity + +| Item | Description | +|------|-------------| +| Application Name | ProjT Launcher | +| Application ID | `org.projecttick.projtlauncher` | +| Organization | Project Tick | + +### Branding + +| Asset | Purpose | +|-------|---------| +| Logo | Application icon | +| Banner | Promotional graphics | +| Screenshots | Store/website images | + +### Endpoints + +| Endpoint | Purpose | +|----------|---------| +| Update URL | Auto-update checks | +| News API | Launcher news feed | +| Analytics | Usage statistics (optional) | + +### Desktop Integration + +| File | Platform | +|------|----------| +| `.desktop` file | Linux | +| `Info.plist` template | macOS | +| Resource files | Windows | + +--- + +## Customization + +When building a custom fork: + +1. Update application name and ID +2. Replace branding assets +3. Configure custom endpoints +4. Update desktop integration files + +--- + +## Related Documentation + +- [LauncherJava](./launcherjava.md) — Java component +- [JavaCheck](./javacheck.md) — Java detection diff --git a/archived/projt-launcher/docs/handbook/ptcieval.md b/archived/projt-launcher/docs/handbook/ptcieval.md new file mode 100644 index 0000000000..b1843f7429 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/ptcieval.md @@ -0,0 +1,122 @@ +# CI Evaluation `ci/eval/` + +> **Location**: `ci/eval/` +> **Platform**: Nix +> **Purpose**: Project configuration validation +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +The CI evaluation module provides Nix-based validation of the project structure and configuration. It catches errors in build files before they reach CI. + +--- + +## What It Validates + +| Component | Checks | +|-----------|--------| +| **CMake** | Syntax, target definitions | +| **vcpkg** | Dependency declarations | +| **Nix** | Flake structure, expressions | +| **Build** | Cross-platform configuration | + +--- + +## Quick Usage + +### Validate Everything + +```bash +nix-build ci -A eval.full +``` + +### Validate Specific Component + +```bash +nix-build ci -A eval.cmake +nix-build ci -A eval.vcpkg +nix-build ci -A eval.nix +``` + +### Quick Test Mode + +```bash +nix-build ci -A eval.validate --arg quickTest true +``` + +--- + +## Supported Systems + +| System | Platform | +|--------|----------| +| `x86_64-linux` | Linux 64-bit | +| `x86_64-darwin` | macOS Intel | +| `aarch64-darwin` | macOS Apple Silicon | +| `x86_64-windows` | Windows (cross) | + +### Limit to Specific System + +```bash +nix-build ci -A eval.full --arg systems '["x86_64-linux"]' +``` + +--- + +## Configuration Validation + +### CMake Files + +- `CMakeLists.txt` — Main configuration +- `cmake/*.cmake` — CMake modules +- `CMakePresets.json` — Build presets + +### Dependencies + +- `vcpkg.json` — vcpkg dependencies +- `vcpkg-configuration.json` — vcpkg settings + +### Nix Build + +- `flake.nix` — Flake definition +- `default.nix` — Default expression +- `shell.nix` — Development shell + +--- + +## CI Integration + +Evaluation runs in `.github/workflows/eval.yml`: + +```yaml +- name: Evaluate + run: | + nix-build --expr 'let + pkgs = import <nixpkgs> {}; + eval = (import ./ci/eval { inherit (pkgs) lib runCommand cmake nix jq; }) {}; + in eval.full' +``` + +--- + +## Local Replication + +```bash +NIX_PATH=nixpkgs=channel:nixos-unstable \ +nix-build --expr 'let + pkgs = import <nixpkgs> {}; + eval = (import ./ci/eval { inherit (pkgs) lib runCommand cmake nix jq; }) {}; +in eval.full' + +cat result/summary.md +``` + +--- + +## Related Documentation + +- [Workflows](./workflows.md) — CI overview +- [CI Support](./ci_support.md) — Support files +- [Nix Packaging](./nix.md) — Nix build system diff --git a/archived/projt-launcher/docs/handbook/ptcigh.md b/archived/projt-launcher/docs/handbook/ptcigh.md new file mode 100644 index 0000000000..f694b90b45 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/ptcigh.md @@ -0,0 +1,95 @@ +# GitHub CI Scripts `ci/github-script/` + +> **Location**: `ci/github-script/` +> **Platform**: GitHub Actions (`actions/github-script`) +> **Purpose**: PR automation and validation +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +This directory contains JavaScript code for GitHub Actions automation using [`actions/github-script`](https://github.com/actions/github-script). These scripts automate various workflow tasks. + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| PR validation | Check PR requirements | +| Auto-labeling | Apply labels based on changes | +| Commit checking | Validate commit messages | +| Reviewer assignment | Auto-assign reviewers | +| Branch management | Merge queue handling | + +--- + +## Scripts + +| Script | Purpose | +|--------|---------| +| `bot.js` | Main bot logic for PR automation | +| `commits.js` | Commit message validation | +| `merge.js` | Merge queue handling | +| `prepare.js` | PR preparation checks | +| `reviewers.js` | Automatic reviewer assignment | +| `reviews.js` | Review status checking | +| `withRateLimit.js` | GitHub API rate limiting | + +--- + +## Local Development + +### Prerequisites + +- Node.js 18+ +- `gh` CLI (authenticated) + +### Setup + +```bash +cd ci/github-script +npm install +``` + +### Running Scripts + +```bash +# Check commits in a PR +./run commits Project-Tick ProjT-Launcher 123 + +# Check labels +./run labels Project-Tick ProjT-Launcher +``` + +--- + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `GITHUB_TOKEN` | GitHub API token | +| `GITHUB_REPOSITORY` | Repository (`owner/repo`) | + +--- + +## Integration + +These scripts are called from workflows in `.github/workflows/`: + +```yaml +- uses: actions/github-script@v7 + with: + script: | + const script = require('./ci/github-script/commits.js'); + await script({ github, context, core }); +``` + +--- + +## Related Documentation + +- [Workflows](./workflows.md) — CI overview +- [Bot](./bot.md) — Cloudflare Worker bot +- [CI Support](./ci_support.md) — Support files diff --git a/archived/projt-launcher/docs/handbook/quazip.md b/archived/projt-launcher/docs/handbook/quazip.md new file mode 100644 index 0000000000..db150433a3 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/quazip.md @@ -0,0 +1,149 @@ +# QuaZip `quazip/` + +> **Type**: ZIP Archive Library (Qt C++ Wrapper) +> **License**: LGPL-2.1-or-later +> **Fork Origin**: [GitHub](https://github.com/stachenov/quazip) +> **Status**: Detached Fork (independently maintained) +> **Dependencies**: Qt 5/6, zlib +> **Latest Version**: 0.0.5-1 + +[![CI](https://github.com/Project-Tick/ProjT-Launcher/actions/workflows/ci-new.yml/badge.svg?branch=develop)](https://github.com/Project-Tick/ProjT-Launcher/actions/workflows/ci-new.yml) + +--- + +## Overview + +QuaZip is the C++ wrapper for Gilles Vollant's ZIP/UNZIP package (Minizip) using the Qt library. If you need to write files to a ZIP archive or read files from one using the `QIODevice` API, QuaZip is the tool you need. + +--- + +## Usage in ProjT Launcher + +QuaZip is used for: + +- **Mod installation** — Extracting mod archives +- **Modpack handling** — Processing CurseForge/Modrinth packs +- **Resource packs** — Managing Minecraft resource packs +- **Backup/restore** — Creating instance backups + +--- + +## Features + +| Feature | Status | +|---------|--------| +| Read/Write ZIP files | ✅ | +| Qt stream integration (`QIODevice`) | ✅ | +| Unicode filename support | ✅ | +| ZIP64 support (files > 4GB) | ✅ | +| BZIP2 compression | ✅ | +| Password protection (read) | ✅ | + +--- + +## Documentation + +| Resource | Description | +|----------|-------------| +| [Migration Guide](../../quazip/QuaZip-1.x-migration.md) | Migration guide to QuaZip 1.x | +| [Online Docs](https://projecttick.org/projtlauncher/wiki/) | Full documentation | +| `quazip/qztest/` | Test suite and examples | + +--- + +## Build Instructions + +### Prerequisites + +- **Qt 5.12+** or **Qt 6.x** +- **zlib** (or Qt's bundled zlib) +- **CMake 3.15+** +- **libbz2** (optional, for BZIP2 support) + +### Linux + +```bash +sudo apt-get install libbz2-dev +cmake -B build +cmake --build build +``` + +### Windows + +When using Qt online installer with MSVC, select `MSVC 20XY 64-bit` and under additional libraries, select `Qt 5 Compatibility Module`. Add `C:\Qt\6.8.2\msvc20XY_64` to your PATH. + +#### Using vcpkg (x64) + +```bat +cmake --preset vcpkg +cmake --build build --config Release +``` + +#### Using Conan v2 (x64) + +```bat +conan install . -of build -s build_type=Release -o *:shared=False --build=missing +cmake --preset conan +cmake --build build --config Release +``` + +### Complete Build Workflow + +```bash +cmake -B build -DQUAZIP_QT_MAJOR_VERSION=6 -DBUILD_SHARED_LIBS=ON -DQUAZIP_ENABLE_TESTS=ON +cmake --build build --config Release +sudo cmake --install build +cd build && ctest --verbose -C Release +``` + +--- + +## CMake Options + +| Option | Description | Default | +|--------|-------------|---------| +| `QUAZIP_QT_MAJOR_VERSION` | Qt version to search for (5 or 6) | Auto-detect | +| `BUILD_SHARED_LIBS` | Build as shared library | `ON` | +| `QUAZIP_LIB_FILE_NAME` | Output library name | `quazip<ver>-qt<ver>` | +| `QUAZIP_INSTALL` | Enable installation | `ON` | +| `QUAZIP_USE_QT_ZLIB` | Use Qt's bundled zlib (not recommended) | `OFF` | +| `QUAZIP_ENABLE_TESTS` | Build test suite | `OFF` | +| `QUAZIP_BZIP2` | Enable BZIP2 compression | `ON` | +| `QUAZIP_BZIP2_STDIO` | Output BZIP2 errors to stdio | `ON` | +| `QUAZIP_ENABLE_QTEXTCODEC` | Enable QTextCodec on Qt6 | `ON` | + +--- + +## Copyright & Licensing + +``` +Copyright (C) 2005-2020 Sergey A. Tachenov and contributors +``` + +Distributed under **LGPL-2.1-or-later**. See `quazip/COPYING` for details. + +Original ZIP package is copyrighted by Gilles Vollant (zlib license). + +--- + +## Contributing + +- **Report bugs**: [GitHub Issues](https://github.com/Project-Tick/ProjT-Launcher/issues) +- **Contribute code**: See [CONTRIBUTING.md](../../CONTRIBUTING.md) +- **Migration guide**: `quazip/QuaZip-1.x-migration.md` + +--- + +## Related Documentation + +- [zlib](./zlib.md) — Underlying compression library +- [bzip2](./bzip2.md) — BZIP2 compression support +- [Third-party Libraries](./third-party.md) — All dependencies + +--- + +## External Links + +- [QuaZip GitHub](https://github.com/stachenov/quazip) +- [Minizip](http://www.winimage.com/zLibDll/minizip.html) +- [Qt Documentation](https://doc.qt.io/) diff --git a/archived/projt-launcher/docs/handbook/third-party.md b/archived/projt-launcher/docs/handbook/third-party.md new file mode 100644 index 0000000000..64e4c809c4 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/third-party.md @@ -0,0 +1,166 @@ +# Libraries + +> **Purpose**: Overview of all external dependencies used by ProjT Launcher + +--- + +## Overview + +ProjT Launcher includes several third-party libraries for various functionality. This document provides a quick reference to all external dependencies. + +--- + +## Detached Fork Libraries + +These libraries are **detached forks** — independently maintained by Project Tick, not synchronized with original repositories: + +### Compression + +| Library | Purpose | License | Documentation | +|---------|---------|---------|---------------| +| **zlib** | DEFLATE compression | zlib | [zlib.md](./zlib.md) | +| **bzip2** | Block-sorting compression | BSD-like | [bzip2.md](./bzip2.md) | +| **QuaZip** | Qt ZIP wrapper | LGPL-2.1+ | [quazip.md](./quazip.md) | + +### Data Formats + +| Library | Purpose | License | Documentation | +|---------|---------|---------|---------------| +| **toml++** | TOML parser | MIT | [tomlplusplus.md](./tomlplusplus.md) | +| **libnbt++** | NBT format | GPL-3.0 | [libnbtplusplus.md](./libnbtplusplus.md) | +| **cmark** | Markdown parser | BSD-2-Clause | [cmark.md](./cmark.md) | + +### Utilities + +| Library | Purpose | License | Documentation | +|---------|---------|---------|---------------| +| **libqrencode** | QR code generation | LGPL-2.1+ | [libqrencode.md](./libqrencode.md) | + +### Build System + +| Library | Purpose | License | Documentation | +|---------|---------|---------|---------------| +| **Extra CMake Modules** | CMake utilities | BSD-3-Clause | [extra-cmake-modules.md](./extra-cmake-modules.md) | + +--- + +## Original READMEs + +Each detached fork retains its original upstream documentation. These are preserved for reference but are not actively maintained by Project Tick: + +| Library | Documentation | +|---------|---------------| +| toml++ | `tomlplusplus/README.md` | +| libqrencode | `libqrencode/README.md` | +| ECM | `extra-cmake-modules/README.md` | +| zlib | `zlib/README-cmake.md`, `zlib/FAQ` | +| bzip2 | `bzip2/bzip2.txt`, `bzip2/manual.html` | +| cmark | `cmark/changelog.txt`, `cmark/man/` | +| QuaZip | `quazip/QuaZip-1.x-migration.md` | +| libnbt++ | `libnbtplusplus/include/` (headers) | + +For Project Tick–specific documentation and usage, refer to the handbook pages linked above. + +--- + +## Internal Libraries + +These are ProjT Launcher-specific libraries: + +### LocalPeer `LocalPeer/` + +Single-instance application enforcement. + +- **Origin**: Derived from QtSingleApplication +- **License**: BSD +- **Use**: Prevents multiple launcher instances + +### murmur2 `murmur2/` + +Hash function implementation. + +- **Origin**: [SMHasher](https://github.com/aappleby/smhasher) +- **License**: Public Domain +- **Use**: Content hashing for mods + +### rainbow `rainbow/` + +Color manipulation functions. + +- **Origin**: KGuiAddons +- **License**: LGPL-2.1+ +- **Use**: Adaptive text coloring + +### systeminfo `systeminfo/` + +System information probing. + +- **License**: Apache-2.0 +- **Use**: Hardware/OS detection + +### qdcss `qdcss/` + +Quick and dirty CSS parser. + +- **Origin**: NilLoader +- **License**: LGPL-3.0 +- **Use**: Mod metadata parsing + +--- + +## External Dependencies + +These are system/Qt dependencies not bundled: + +### Qt Framework + +| Module | Purpose | +|--------|---------| +| Qt Core | Base functionality | +| Qt GUI | UI rendering | +| Qt Widgets | UI components | +| Qt Network | HTTP/networking | +| Qt Concurrent | Threading | + +### System Libraries + +| Library | Purpose | Platforms | +|---------|---------|-----------| +| OpenSSL | TLS/HTTPS | All | +| libpng | PNG images | All | +| zlib (system) | Alternative to bundled | Optional | + +--- + +## License Summary + +| License | Libraries | +|---------|-----------| +| **MIT** | toml++ | +| **BSD-2/3-Clause** | cmark, gamemode | +| **BSD-like** | bzip2, LocalPeer | +| **zlib** | zlib | +| **LGPL-2.1+** | QuaZip, rainbow, libqrencode | +| **LGPL-3.0** | qdcss | +| **GPL-3.0** | libnbt++ | +| **Apache-2.0** | systeminfo | +| **Public Domain** | murmur2 | + +--- + +## Adding New Dependencies + +When adding new libraries: + +1. **Evaluate license compatibility** with GPL-3.0 +2. **Create handbook documentation** in `docs/handbook/` +3. **Update this file** with the new entry +4. **Add CI workflow** if needed +5. **Update CMakeLists.txt** for build integration + +--- + +## Related Documentation + +- [Workflows](./workflows.md) — CI for libraries +- [README](./README.md) — Handbook index diff --git a/archived/projt-launcher/docs/handbook/tomlplusplus.md b/archived/projt-launcher/docs/handbook/tomlplusplus.md new file mode 100644 index 0000000000..bf3d168977 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/tomlplusplus.md @@ -0,0 +1,229 @@ +# toml++ `tomlplusplus/` + +> **Type**: TOML Parser Library +> **License**: MIT +> **Fork Origin**: [GitHub](https://github.com/marzer/tomlplusplus) +> **Status**: Detached Fork (independently maintained) +> **Specification**: [TOML v1.0.0](https://toml.io/en/v1.0.0) +> **Latest Version**: 0.0.5-1 + +[![CI](https://github.com/Project-Tick/ProjT-Launcher/actions/workflows/ci-new.yml/badge.svg)](https://github.com/Project-Tick/ProjT-Launcher/actions/workflows/ci-new.yml) + +--- + +## Overview + +toml++ is a header-only TOML configuration file parser and serializer for C++17 and later. It provides a clean, intuitive API for reading and writing TOML files with full TOML v1.0.0 compliance. + +ProjT Launcher maintains a detached fork for independent development and CI integration. + +--- + +## Usage in ProjT Launcher + +toml++ is used for: + +- **Configuration files** — Reading launcher settings +- **Instance configs** — Parsing instance metadata +- **Mod manifests** — Processing `fabric.mod.json` alternatives +- **Pack metadata** — Reading modpack configuration + +--- + +## Features + +| Feature | Status | +|---------|--------| +| TOML v1.0.0 compliant | ✅ | +| Header-only library | ✅ | +| C++17/20/23 support | ✅ | +| Unicode support | ✅ | +| Serialize to TOML | ✅ | +| Parse from string/file | ✅ | +| Error messages with line numbers | ✅ | +| Compile-time optimizations | ✅ | + +--- + +## Quick Start + +### Single Header Include + +```cpp +#include <toml++/toml.hpp> +``` + +### Parse TOML + +```cpp +// From string +auto config = toml::parse(R"( + [server] + host = "localhost" + port = 8080 +)"); + +// From file +auto config = toml::parse_file("config.toml"); + +// Access values +std::string host = config["server"]["host"].value_or("127.0.0.1"); +int port = config["server"]["port"].value_or(80); +``` + +### Create TOML + +{% raw %} +```cpp +auto tbl = toml::table{{ + {"server", toml::table{{ + {"host", "localhost"}, + {"port", 8080} + }}} +}}; + +std::cout << tbl << std::endl; +``` +{% endraw %} + +--- + +## Build Integration + +### CMake (Recommended) + +toml++ is header-only, but provides a CMake target: + +```cmake +# Add as subdirectory +add_subdirectory(tomlplusplus) + +# Or find installed package +find_package(tomlplusplus REQUIRED) + +# Link to your target +target_link_libraries(your_target PRIVATE tomlplusplus::tomlplusplus) +``` + +### Include Directly + +```cmake +target_include_directories(your_target PRIVATE path/to/tomlplusplus/include) +``` + +### Compiler Requirements + +| Compiler | Minimum Version | +|----------|-----------------| +| GCC | 8+ | +| Clang | 8+ | +| MSVC | 19.20+ (VS 2019) | +| ICC | 19.11+ | + +--- + +## Configuration Options + +Define before including to customize behavior: + +```cpp +// Disable exceptions +#define TOML_EXCEPTIONS 0 + +// Disable optional features +#define TOML_ENABLE_FORMATTERS 0 +#define TOML_ENABLE_PARSER 0 // Write-only mode + +// Unicode handling +#define TOML_ASSUME_UTF8_EVERYWHERE 1 +``` + +--- + +## Error Handling + +### With Exceptions (Default) + +```cpp +try { + auto config = toml::parse_file("config.toml"); +} catch (const toml::parse_error& err) { + std::cerr << "Parse error at " << err.source().begin << ":\n" + << err.description() << std::endl; +} +``` + +### Without Exceptions + +```cpp +#define TOML_EXCEPTIONS 0 + +toml::parse_result result = toml::parse_file("config.toml"); +if (!result) { + std::cerr << "Parse error: " << result.error() << std::endl; + return; +} +toml::table& config = result.table(); +``` + +--- + +## Testing + +toml++ includes a comprehensive test suite: + +```bash +cd tomlplusplus +mkdir build && cd build +cmake .. -DTOML_BUILD_TESTS=ON +cmake --build . +ctest -V +``` + +See [ci-tomlplusplus.yml](../../.github/workflows/ci-tomlplusplus.yml) for CI configuration. + +--- + +## Documentation + +| Resource | Description | +|----------|-------------| +| [API Docs](https://projecttick.org/tomlplusplus/) | Full API reference | +| [TOML Spec](https://toml.io/en/v1.0.0) | TOML v1.0.0 specification | +| [Original README](../../tomlplusplus/README.md) | Upstream library documentation | +| [Website Build](./website-tomlplusplus.md) | Building API docs | + +--- + +## Copyright & Licensing + +``` +Copyright (c) Mark Gillard <mark.gillard@outlook.com.au> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software to deal in the Software without restriction, including the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. +``` + +Full license: `tomlplusplus/LICENSE` + +--- + +## Related Documentation + +- [cmark](./cmark.md) — Markdown parser (similar use case) +- [libnbt++](./libnbtplusplus.md) — NBT format parser +- [Website toml++ Docs](./website-tomlplusplus.md) — Building documentation +- [Third-party Libraries](./third-party.md) — All dependencies + +--- + +## External Links + +- [toml++ GitHub](https://github.com/marzer/tomlplusplus) +- [TOML Official Site](https://toml.io/) +- [toml++ Online Docs](https://marzer.github.io/tomlplusplus/) +- [TOML Test Suite](https://github.com/BurntSushi/toml-test) diff --git a/archived/projt-launcher/docs/handbook/website-tomlplusplus.md b/archived/projt-launcher/docs/handbook/website-tomlplusplus.md new file mode 100644 index 0000000000..0fbdeeac90 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/website-tomlplusplus.md @@ -0,0 +1,124 @@ +# toml++ Documentation Setup `website/tomlplusplus/` + +> **Location**: `website/tomlplusplus/` +> **Tools**: Poxy, Doxygen, m.css +> **Output**: API documentation HTML +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +This directory contains configuration for building the toml++ API documentation using Poxy (a Doxygen + m.css wrapper). + +--- + +## Quick Start + +### Install Dependencies + +```bash +npm run setup:doxygen +``` + +This installs: +- **Doxygen** (via Homebrew on macOS, apt on Linux) +- **Poxy** (via pipx) + +### Build Documentation + +```bash +npm run build:tomlplusplus +``` + +### Full Site Build + +```bash +npm run build # Includes toml++ docs +``` + +--- + +## Manual Setup + +### macOS + +```bash +brew install doxygen pipx +pipx install poxy +``` + +### Linux + +```bash +sudo apt-get install doxygen python3-pip +python3 -m pip install --user pipx +pipx install poxy +``` + +--- + +## Configuration + +Documentation is configured via `poxy.toml`: + +| Setting | Value | +|---------|-------| +| Source paths | `../../tomlplusplus/include` | +| Pages | `pages/` directory | +| Images | `images/` directory | +| Theme | Dark (default) | +| Output | `html/` directory | + +--- + +## Directory Structure + +``` +website/tomlplusplus/ +├── poxy.toml # Poxy configuration +├── pages/ # Documentation pages (Markdown) +│ └── main_page.md +├── images/ # Badges, logos, graphics +└── html/ # Generated output (after build) +``` + +--- + +## Build Process + +1. **Poxy** reads `poxy.toml` +2. **Doxygen** parses C++ headers +3. **m.css** generates styled HTML +4. Output copied to `_site/tomlplusplus/` during site build + +--- + +## Troubleshooting + +### ModuleNotFoundError: jinja2 + +Install to Homebrew Python: + +```bash +/opt/homebrew/bin/python3 -m pip install --break-system-packages jinja2 Pygments +``` + +### Doxygen Warnings + +Minor warnings about unresolved links are normal and don't prevent generation. + +--- + +## Related Documentation + +- [toml++](./tomlplusplus.md) — Library documentation +- [Workflows](./workflows.md) — CI builds documentation + +--- + +## External Links + +- [Poxy Documentation](https://github.com/marzer/poxy) +- [Doxygen Manual](https://www.doxygen.nl/manual/) +- [m.css](https://mcss.mosra.cz/) diff --git a/archived/projt-launcher/docs/handbook/wiki/development/index.md b/archived/projt-launcher/docs/handbook/wiki/development/index.md new file mode 100644 index 0000000000..27f68678c7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/development/index.md @@ -0,0 +1,21 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Development + +If you would like to contribute to ProjT Launcher, you might find it useful to join our Discord Server or Matrix Space. + +## Building + +If you want to build ProjT Launcher yourself and/or contribute, check out our [Build Instructions](./build-instructions) page for a handy guide. + +## Code formatting + +Please follow the existing formatting. + +In general, by order of importance: + +- Make sure your IDE is not messing up line endings or whitespace and avoid using linters. +- Prefer readability to dogma. +- Keep to the existing formatting. +- Indent with 4 space unless it's in a submodule. +- Keep lists (of arguments, parameters, initializers...) as lists, not paragraphs. It should either read from top to bottom, or left to right. Not both. diff --git a/archived/projt-launcher/docs/handbook/wiki/development/instructions/index.md b/archived/projt-launcher/docs/handbook/wiki/development/instructions/index.md new file mode 100644 index 0000000000..cbe5817943 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/development/instructions/index.md @@ -0,0 +1,23 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +<script> + function appendToURL(url, path) { + if (!url.pathname.endsWith("/")) { + url.pathname += "/"; // if there isn't a slash, add it + } + url.pathname += path; + } + + let url = new URL(window.location.href); + // redirect the user to the correct place + if (navigator.platform.indexOf("Mac") !== -1) { + appendToURL(url, "macos"); + window.location.href = url.href; + } else if (navigator.platform.indexOf("nix") !== -1 || navigator.platform.indexOf("nux") !== -1 || navigator.userAgent.indexOf("X11") !== -1) { + appendToURL(url, "linux"); + window.location.href = url.href; + }else{ + appendToURL(url, "windows"); + window.location.href = url.href; + } +</script> diff --git a/archived/projt-launcher/docs/handbook/wiki/development/instructions/linux.md b/archived/projt-launcher/docs/handbook/wiki/development/instructions/linux.md new file mode 100644 index 0000000000..9c6a771c3f --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/development/instructions/linux.md @@ -0,0 +1,260 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Linux Build Instructions + +## Contents + +[[toc]] + +## Getting the source + +Clone the source code using git, and grab all the submodules: + +```bash +git clone --recursive https://github.com/Project-Tick/ProjT-Launcher.git +cd ProjT-Launcher +``` + +**The rest of the documentation assumes you have already cloned the repository.** + +## Building + +Getting the project to build and run on Linux is easy if you use any modern and up-to-date Linux distribution. + +### Build dependencies + +- A C++ compiler capable of building C++17 code. +- Qt Development tools 6.0 or newer (`qt6-base-dev qtchooser qt6-base-dev-tools libqt6core6 libqt6core5compat6-dev libqt6network6 qt6-networkauth-dev` on Debian (testing/unstable) based systems). +- Alternatively, you can also use Qt 5.12 or newer (`qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5-dev` on Debian-based systems), if you prefer it. +- cmake 3.15 or newer (`cmake` on Debian-based system) +- ninja (`ninja-build` on Debian-based systems) +- extra-cmake-modules (`extra-cmake-modules` on Debian-based system) +- zlib (`zlib1g-dev` on Debian-based system) +- Java JDK (`openjdk-17-jdk` on Debian-based system) +- GL headers (`libgl1-mesa-dev` on Debian-based system) +- scdoc if you want to generate manpages (`scdoc` on Debian-based system) + +You can use IDEs, like KDevelop, QtCreator or CLion to open the CMake project, if you want to work on the code. + +### Building a portable binary + +```bash +cmake -S . -B build -G Ninja \ + -DCMAKE_INSTALL_PREFIX=install +# -DLauncher_QT_VERSION_MAJOR="5" # if you want to use Qt 5 + +cmake --build build +cmake --install build +cmake --install build --component portable +``` + +### Building & installing to the system + +This is the preferred method of installation, and is suitable for packages. + +```bash +cmake -S . -B build -G Ninja \ +혻 -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="/usr" \ # Use "/usr" when building Linux packages. If building not for package, use "/usr/local" + -DENABLE_LTO=ON # if you want to enable LTO/IPO +# -DLauncher_QT_VERSION_MAJOR="5" # if you want to use Qt 5 + +cmake --build build +cmake --install build # Optionally specify DESTDIR for packages (i.e. DESTDIR=${pkgdir} cmake --install ...) +``` + +<!-- ### Building a .deb + +Requirements: [makedeb](https://docs.makedeb.org/) installed on your system. + +```bash +git clone https://mpr.makedeb.org/projtlauncher.git +cd projtlauncher +makedeb -s +``` + +The .deb will be located in the directory the repo was cloned in. + +### Building an .rpm for Fedora + +Build dependencies are automatically installed using `DNF`, however, you will also need the `rpmdevtools` package (on Fedora), +in order to fetch sources and set up your tree. +You don't need to clone the repo for this; the spec file handles that. + +```bash +cd ~ +# setup your ~/rpmbuild directory, required for rpmbuild to work. +rpmdev-setuptree +# get the rpm spec file from the projtlauncher on pagure +git clone https://pagure.io/projtlauncher.git +cd projtlauncher +# install build dependencies +sudo dnf builddep projtlauncher.spec +sudo dnf builddep -D "_without_qt6 1" projtlauncher.spec # if you want to use Qt 5 instead of Qt 6 +# download build sources +spectool -g -R projtlauncher.spec +# move patches to rpmbuild sources directory +cp *.patch ~/rpmbuild/SOURCES +# copy any patches to rpmbuild sources directory +cp *.patch ~/rpmbuild/SOURCES +# now build! +rpmbuild -bb projtlauncher.spec +rpmbuild -bb --without qt6 projtlauncher.spec # if you want to use Qt 5 instead of Qt 6 +``` + +The path to the .rpm packages will be printed once the build is complete. + +### Building an .rpm for openSUSE + +Unlike Fedora, the openSUSE packages depend on the [Open Build Service](https://build.opensuse.org/), so you will need to install the command line tool `osc` by following [this](https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.osc.html#sec.obs.osc.install) guide. +It also uses the [obs_scm](https://github.com/openSUSE/obs-service-tar_scm) service, which is available in the `obs-service-obs_scm` package if it's not already installed. + +```bash +osc checkout home:getchoo + +# there will be 4 directories in home:getchoo, with some having a -qt5 and/or -nightly suffix +# -qt5 packages will build with Qt 5 instead of Qt 6, while -nightly packages will build with the latest commit (updated every 24h) +# for this example, we're just using the stable release package that builds with Qt 6 +# NOTE: only -qt5 will build on Leap +cd home:getchoo/projtlauncher + +# to build against the current version of Leap, replace `openSUSE_Tumbleweed` with 15.4 +osc build --sccache openSUSE_Tumbleweed +``` + +The path to the .rpm packages will be printed once the build is complete. + +### Building a Flatpak + +You don't need to clone the entire ProjT Launcher repo for the latest stable version; the Flatpak file handles that. However, cloning the source repository is necessary to build from the latest commit (contains upstream manifest). +Both `flatpak` and `flatpak-builder` packages must be installed on your system to proceed, including all build dependencies previously mentioned (at the top of page). + +#### Latest Stable Release + +```bash +git clone --recursive https://github.com/flathub/org.projecttick.ProjTLauncher +cd org.projecttick.ProjTLauncher +flatpak install org.kde.Sdk/x86_64/6.7 runtime/org.freedesktop.Sdk.Extension.openjdk17/x86_64/23.08 runtime/org.freedesktop.Sdk.Extension.openjdk8/x86_64/23.08 runtime/org.freedesktop.Sdk.Extension.openjdk21/x86_64/23.08 # build requirements +# remove --user --install if you want to build without installing +flatpak-builder --user --install flatbuild org.projecttick.ProjTLauncher.yml +``` + +#### Latest Commit + +```bash +git clone --recursive https://github.com/Project-Tick/ProjT-Launcher # source repo - contains upstream manifest +cd ProjTLauncher/flatpak +flatpak install org.kde.Sdk/x86_64/6.7 runtime/org.freedesktop.Sdk.Extension.openjdk17/x86_64/23.08 runtime/org.freedesktop.Sdk.Extension.openjdk8/x86_64/23.08 runtime/org.freedesktop.Sdk.Extension.openjdk21/x86_64/23.08 # build requirements +# remove --user --install if you want to build without installing +flatpak-builder --user --install flatbuild org.projecttick.ProjTLauncher.yml --> + +### Installing Qt using the installer (optional) + +1. Run the Qt installer. +2. Choose a place to install Qt. +3. Choose the components that you wish install. + + - You need Qt 6.0.x 64-bit ticked. (or a newer version) + - Alternatively you can choose Qt 5.12.0 or newer + - You need Tools/Qt Creator ticked. + - Other components are selected by default, you can un-tick them if you don't need them. + +4. Accept the license agreements. +5. Double-check the install details and then click "Install". + + - Installation can take a very long time, go grab a cup of tea or something and let it work. + +## IDEs and Tooling + +There are a few tools that you can set up to make your development workflow smoother. In addition, some IDEs also require a bit more setup to work with Qt and CMake. + +### ccache + +**ccache** is a compiler cache. It speeds up recompilation by caching previous compilations and detecting when the same compilation is being done again. + +You can [download it here](https://ccache.dev/download.html). After setting up, builds will be incremental, and the builds after the first one will be much faster. + +### VS Code + +To set up VS Code, you can download [the C/C++ extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools), since it provides IntelliSense auto complete, linting, formatting, and various other features. + +Then, you need to set up the configuration. Go into the command palette and open up C/C++: Edit Configurations (UI). There, add a new configuration for ProjTLauncher. + +1. Add the path to your Qt `include` folder to `includePath` +2. Add `-L/{path to your Qt installation}/lib` to `compilerArgs` +3. Set `compileCommands` to `${workspaceFolder}/build/compile_commands.json` +4. Set `cppStandard` to `c++14` or higher. + +For step 3 to work, you also have to reconfigure CMake to generate a `compile_commands.json` file. To do this, add `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to the end of your CMake configuration command and run it again. You should see a file at `build/compile_commands.json`. + +Now the VS Code setup should be fully working. To test, open up some files and see if any error squiggles appear. If there are none, it's working properly! + +Here is an example of what `.vscode/c_cpp_properties.json` looks like on macOS with Qt installed via Homebrew: + +```json +{ + "configurations": [ + { + "name": "Mac (ProjTLauncher)", + "includePath": [ + "${workspaceFolder}/**", + "/opt/homebrew/opt/qt@6/include/**" + ], + "defines": [], + "macFrameworkPath": [ + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks" + ], + "compilerPath": "/usr/bin/clang", + "compilerArgs": [ + "-L/opt/homebrew/opt/qt@6/lib" + ], + "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "macos-clang-arm64" + } + ], + "version": 4 +} +``` + +### CLion + +1. Open CLion +2. Choose `File->Open` +3. Navigate to the source folder +4. Go to settings `Ctrl+Alt+S` +5. Navigate to `Toolchains` in `Build, Execution, Deployment` + - Set the correct build tools ([see here](https://i.imgur.com/daFAdVe.png)) + - CMake: `cmake` (optional) + - Make: `make` (optional) + - C Compiler: `gcc` + - C++ Compiler: `g++` + - Debugger: `gdb` (optional) +6. Navigate to `CMake` in `Build, Execution, Deployment` + - Set `Build directory` to `build` +7. Navigate to `Edit Configurations` ([see here](https://i.imgur.com/fu53nc3.png)) + - Create a new configuration + - Name: `All` + - Target: `All targets` + - Choose the newly added configuration as default + +Now you should be able to build and test ProjT Launcher with the `Build` and `Run` buttons. + +### Qt Creator + +1. Open Qt Creator. +2. Choose `File->Open File or Project`. +3. Navigate to the Launcher source folder you cloned and choose CMakeLists.txt. +4. Read the instructions that just popped up about a build location and choose one. +5. You should see "Run CMake" in the window. + + - Make sure that Generator is set to "Unix Generator (Desktop Qt 6.x.x GCC 64bit)". + - Alternatively this is probably "Unix Generator (Desktop Qt 5.12.x GCC 64bit)" + - Hit the "Run CMake" button. + - You'll see warnings, and it might not be clear that it succeeded until you scroll to the bottom of the window. + - Hit "Finish" if CMake ran successfully. + +6. Cross your fingers, and press the "Run" button (bottom left of Qt Creator). + + - If the project builds successfully it will run and the Launcher window will pop up. diff --git a/archived/projt-launcher/docs/handbook/wiki/development/instructions/macos.md b/archived/projt-launcher/docs/handbook/wiki/development/instructions/macos.md new file mode 100644 index 0000000000..a9cb92461d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/development/instructions/macos.md @@ -0,0 +1,162 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Build Instructions + +## Contents + +[[toc]] + +## Getting the source + +Clone the source code using git, and grab all the submodules: + +```zsh +git clone --recursive https://github.com/Project-Tick/ProjT-Launcher.git +cd ProjT-Launcher +``` + +**The rest of the documentation assumes you have already cloned the repository.** + +## Building + +### Install prerequisites + +- Install XCode Command Line tools. +- Install the official build of CMake (<https://cmake.org/download/>). +- Install extra-cmake-modules +- Install JDK 8 (<https://adoptium.net/temurin/releases/?variant=openjdk8&jvmVariant=hotspot>). +- Install any version of Qt 6 (recommended) or Qt 5.12 or newer + +Using [homebrew](https://brew.sh) you can install these dependencies with a single command: + +```zsh +brew update # in the case your repositories weren't updated +brew install qt openjdk@17 cmake ninja extra-cmake-modules # use qt@5 if you want to install qt5 +``` + +### XCode Command Line tools + +If you don't have XCode Command Line tools installed, you can install them with this command: + +```zsh +xcode-select --install +``` + +### Build + +Choose an installation path. + +This is where the final `ProjT-Launcher.app` will be constructed when you run `make install`. Supply it as the `CMAKE_INSTALL_PREFIX` argument during CMake configuration. By default, it's in the dist folder, under ProjT-Launcher. + +[If you are on zsh](https://support.apple.com/kb/HT208050),zsh does not ignore comments by default, run the following to ignore comments for this session: + +```zsh +setopt interactivecomments +``` + +```zsh +mkdir build +cmake \ + -S . \ + -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX:PATH="$(dirname $PWD)/dist/" \ + -DCMAKE_INSTALL_PREFIX="dist" \ + -DCMAKE_PREFIX_PATH="/path/to/Qt/" \ + -DQt5_DIR="/path/to/Qt/" \ + -DQt6_DIR="/path/to/Qt/" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=11 \ + -DLauncher_QT_VERSION_MAJOR=6 \ # if you want to use Qt 6 + -DENABLE_LTO=ON \ # if you want to enable LTO/IPO + -DLauncher_BUILD_PLATFORM=macOS \ +# if you want to enable LTO/IPO: + -DENABLE_LTO=ON +#-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" # to build a universal binary (not recommended for development) +#-DLauncher_QT_VERSION_MAJOR=5 # if you want to use Qt 5 + +cmake --build build +cmake --install build +``` + +Remember to replace `/path/to/Qt/` with the actual path. For newer Qt installations, it is often in your home directory. For the Homebrew installation, it's likely to be in `/opt/homebrew/opt/qt`. + +**Note:** The final app bundle may not run due to code signing issues, which +need to be fixed with `codesign -fs -`. + +## IDEs and Tooling + +There are a few tools that you can set up to make your development workflow smoother. In addition, some IDEs also require a bit more setup to work with Qt and CMake. + +### ccache + +**ccache** is a compiler cache. It speeds up recompilation by caching previous compilations and detecting when the same compilation is being done again. + +You can [download it here](https://ccache.dev/download.html). After setting up, builds will be incremental, and the builds after the first one will be much faster. + +### VS Code + +To set up VS Code, you can download [the C/C++ extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools), since it provides IntelliSense auto complete, linting, formatting, and various other features. + +Then, you need to set up the configuration. Go into the command palette and open up C/C++: Edit Configurations (UI). There, add a new configuration for ProjTLauncher. + +1. Add the path to your Qt `include` folder to `includePath` +2. Add `-L/{path to your Qt installation}/lib` to `compilerArgs` +3. Set `compileCommands` to `${workspaceFolder}/build/compile_commands.json` +4. Set `cppStandard` to `c++14` or higher. + +For step 3 to work, you also have to reconfigure CMake to generate a `compile_commands.json` file. To do this, add `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to the end of your CMake configuration command and run it again. You should see a file at `build/compile_commands.json`. + +Now the VS Code setup should be fully working. To test, open up some files and see if any error squiggles appear. If there are none, it's working properly! + +Here is an example of what `.vscode/c_cpp_properties.json` looks like on macOS with Qt installed via Homebrew: + +```json +{ + "configurations": [ + { + "name": "Mac (ProjTLauncher)", + "includePath": [ + "${workspaceFolder}/**", + "/opt/homebrew/opt/qt@6/include/**" + ], + "defines": [], + "macFrameworkPath": [ + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks" + ], + "compilerPath": "/usr/bin/clang", + "compilerArgs": [ + "-L/opt/homebrew/opt/qt@6/lib" + ], + "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "macos-clang-arm64" + } + ], + "version": 4 +} +``` + +### CLion + +1. Open CLion +2. Choose `File->Open` +3. Navigate to the source folder +4. Go to settings `Ctrl+Alt+S` +5. Navigate to `Toolchains` in `Build, Execution, Deployment` + - Set the correct build tools ([see here](https://i.imgur.com/daFAdVe.png)) + - CMake: `cmake` (optional) + - Make: `make` (optional) + - C Compiler: `gcc` + - C++ Compiler: `g++` + - Debugger: `gdb` (optional) +6. Navigate to `CMake` in `Build, Execution, Deployment` + - Set `Build directory` to `build` +7. Navigate to `Edit Configurations` ([see here](https://i.imgur.com/fu53nc3.png)) + - Create a new configuration + - Name: `All` + - Target: `All targets` + - Choose the newly added configuration as default + +Now you should be able to build and test ProjT Launcher with the `Build` and `Run` buttons. diff --git a/archived/projt-launcher/docs/handbook/wiki/development/instructions/windows.md b/archived/projt-launcher/docs/handbook/wiki/development/instructions/windows.md new file mode 100644 index 0000000000..dfe3b4733a --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/development/instructions/windows.md @@ -0,0 +1,201 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Build Instructions + +## Contents + +[[toc]] + +## Getting the source + +Clone the source code using git, and grab all the submodules: + +```bash +git clone --recursive https://github.com/Project-Tick/ProjT-Launcher.git +cd ProjT-Launcher +``` + +**The rest of the documentation assumes you have already cloned the repository.** + +## Building With MSVC + +### Dependencies + +- [Visual Studio](https://visualstudio.microsoft.com/downloads/) - Software Distribution and Building Platform for Windows + - If you don't want install the Visual Studio IDE, go to 'Tools For Visual Studio' and download 'Build Tools for Visual Studio' instead + - Select 'Desktop development with C++', note that in the optional components (right side) CMake will be selected +- [Java Development Kit 8 or later](https://adoptium.net/) + - Make sure that "Set JAVA_HOME variable" is enabled in the Adoptium installer. +- [Qt](https://www.qt.io/download-qt-installer) + - For Qt 6 (Qt 6.6.2 is the recommended one), 'Qt 5 Compatibility Module' & 'Qt Image Formats' are required + - For Qt 5 (Qt 5.15.2 is the recommended one), OpenSSL Toolkit is required + - If you don't want to use the Qt installer, than you can use [aqt](https://github.com/miurahr/aqtinstall), see [aqt-list](https://ddalcino.github.io/aqt-list-server/) for help with command arguments. + +### Compile from command line on Windows using msbuild + +You will need to run commands from `x64 Native Tools Command Prompt` or `x86 Native Tools Command Prompt` depending on if you are building 64bit or 32bit. +These instructions assume you are using the `x64 Native Tools Command Prompt` to build for 64bit. +All commands are for a debug build, for release builds, replace `Debug` with `Release` in the cmake build and install commands. + +1. `cd` into the folder you cloned ProjT Launcher to. Put quotation marks around the path. +2. Now we can prepare the build itself: Run `cmake -Bbuild -DCMAKE_INSTALL_PREFIX=install -DENABLE_LTO=ON -DCMAKE_PREFIX_PATH=C:\Qt\6.6.2\msvc2019_64\lib\cmake`. These options will copy the final build to the `install` folder after the build. + + - If you have installed Qt in a non-default location, then change the `CMAKE_PREFIX_PATH` to `-DCMAKE_PREFIX_PATH=<Path to Qt Install>\6.6.2\msvc2019_64\lib\cmake`, replacing `<Path to Qt Install>` with the path to your Qt install. + - If you are building for 32bit, change `msvc2019_64` to `msvc2019`. + - If you want to build using Qt 5, then add the `-DLauncher_QT_VERSION_MAJOR=5` parameter and change `CMAKE_PREFIX_PATH` to point to Qt 5. + +3. Now you need to run the build itself: Run `cmake --build build --config Debug -- /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true`. + + - If preferred, `UseMultiToolTask` & `EnforceProcessCountAcrossBuilds` can be set as environment variables instead of passing as arguments. + +4. Now, wait for it to compile. This could take some time, so hopefully it compiles properly. +5. Run the command `cmake --install build --config Debug`, and it should install ProjT Launcher to whatever the `-DCMAKE_INSTALL_PREFIX` was. +6. If you don't want ProjT Launcher to store its data in `%APPDATA%`, run `cmake --install build --config Debug --component portable` after the install process. +7. When building on Qt 5, whenever compiling, the OpenSSL DLLs aren't put into the directory to where ProjT Launcher installs which are necessary in that case, meaning that you cannot log in. The best way to fix this, is just to do `robocopy D:/Qt/Tools/OpenSSL/Win_x64/bin/ install libcrypto-1_1-x64.dll libssl-1_1-x64.dll`. This should copy the required OpenSSL DLLs to log in. When building on Qt 6 this is not necessary because it can use schannel, the Windows tls library. + + - Replace `<Path to Qt Install>` with the path to your Qt install. + - If building for 32bit, replace `Win_x64` with `Win_x86` and remove `-x64` from the dlls names. + +#### Using ccache + +CMake with the msbuild generator currently does not support `CMAKE_CXX_COMPILER_LAUNCHER`, so the process of setting up [ccache](#ccache) differs from other build systems. +ccache 4.7.x or newer is required for MSVC support. + +1. Copy `ccache.exe` and rename that copy to `cl.exe` +2. In the build command, add `/p:TrackFileAccess=false /p:CLToolExe=cl.exe /p:CLToolPath=<path to ccache cl>` to the end of the build arguments + + - Replace `<path to ccache cl>` with the path to the copy of `ccache.exe` you renamed to `cl.exe` + +**If this doesn't work for you, please let us know on our Discord sever, or Matrix Space.** + +## Building with MSYS2 + +### MSYS2 Dependencies + +- [MSYS2](https://www.msys2.org/) - Software Distribution and Building Platform for Windows +- [Java Development Kit 8 or later](https://adoptium.net/) + - Make sure that "Set JAVA_HOME variable" is enabled in the Adoptium installer. + +### Preparing MSYS2 + +1. Open one of the shortcuts from the MSYS2 folder in the Start menu + + - We recommend building using the CLANG64 msystem of MSYS2, as it compiles considerably faster and with a few less bugs. + +2. Install helpers: Run `pacman -Syu pactoys git mingw-w64-x86_64-binutils` in the MSYS2 shell. +3. Install all build dependencies using `pacboy`: Run `pacboy -S toolchain:p cmake:p ninja:p qt6-base:p qt6-5compat:p qt6-svg:p qt6-imageformats:p quazip-qt6:p extra-cmake-modules:p ninja:p ccache:p`. + + - Alternatively you can use Qt 5 (for older Windows versions), by running the following command instead: `pacboy -S toolchain:p cmake:p ninja:p qt5-base:p qt5-svg:p qt5-imageformats:p quazip-qt5:p extra-cmake-modules:p ninja:p ccache:p` + - This might take a while, as it will install Qt and all the build tools required. + +### Compile from command line on Windows + +1. `cd` into the folder you cloned ProjT Launcher to. Put quotation marks around the path. +2. Now we can prepare the build itself: Run `cmake -Bbuild -DCMAKE_INSTALL_PREFIX=install -DENABLE_LTO=ON -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DCMAKE_BUILD_TYPE=Debug -G Ninja`. These options will copy the final build to the `install` folder after the build. + + - Replace Debug with Release if you want to build a Release build. + - If you want to build using Qt 5, then add the `-DLauncher_QT_VERSION_MAJOR=5` parameter + - If you want to use [ccache](#ccache) to speed up recompilations, add the parameter `-DCMAKE_CXX_COMPILER_LAUNCHER=ccache` + +3. Now you need to run the build itself: Run `cmake --build build`. +4. Now, wait for it to compile. This could take some time, so hopefully it compiles properly. +5. Run the command `cmake --install build`, and it should install ProjT Launcher to whatever the `-DCMAKE_INSTALL_PREFIX` was. +6. If you don't want ProjT Launcher to store its data in `%APPDATA%`, run `cmake --install build --component portable` after the install process +7. When building on Qt 5, whenever compiling, the OpenSSL DLLs aren't put into the directory to where ProjT Launcher installs which are necessary in that case, meaning that you cannot log in. The best way to fix this, is just to do `cp /(msystem)/bin/libcrypto-1_1.dll /(msystem)/bin/libssl-1_1.dll install`. This should copy the required OpenSSL DLLs to log in. When building on Qt 6 this is not necessary because it can use schannel, the Windows tls library. + + - Replace `(msystem)` with the msystem you're using (e.g. clang64). On 64-bit msystems, like *MSYS2 CLANG64*, you have to add `-x64` to the dlls. + +**If this doesn't work for you, please let us know on our Discord server, or Matrix Space.** + +## IDEs and Tooling + +There are a few tools that you can set up to make your development workflow smoother. In addition, some IDEs also require a bit more setup to work with Qt and CMake. + +### ccache + +**ccache** is a compiler cache. It speeds up recompilation by caching previous compilations and detecting when the same compilation is being done again. + +You can [download it here](https://ccache.dev/download.html). After setting up, builds will be incremental, and the builds after the first one will be much faster. + +<!-- TODO: The VS-Code instructions could be done better for windows (and in general) --> + +### VS Code + +To set up VS Code, you can download [the C/C++ extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools), since it provides IntelliSense auto complete, linting, formatting, and various other features. + +Then, you need to set up the configuration. Go into the command palette and open up C/C++: Edit Configurations (UI). There, add a new configuration for ProjT-Launcher. + +1. Add the path to your Qt `include` folder to `includePath` +2. Add `-L/{path to your Qt installation}/lib` to `compilerArgs` +3. Set `compileCommands` to `${workspaceFolder}/build/compile_commands.json` +4. Set `cppStandard` to `c++14` or higher. + +For step 3 to work, you also have to reconfigure CMake to generate a `compile_commands.json` file. To do this, add `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` to the end of your CMake configuration command and run it again. You should see a file at `build/compile_commands.json`. + +Now the VS Code setup should be fully working. To test, open up some files and see if any error squiggles appear. If there are none, it's working properly! + +Here is an example of what `.vscode/c_cpp_properties.json` looks like on macOS with Qt installed via Homebrew: + +```json +{ + "configurations": [ + { + "name": "Mac (ProjTLauncher)", + "includePath": [ + "${workspaceFolder}/**", + "/opt/homebrew/opt/qt@6/include/**" + ], + "defines": [], + "macFrameworkPath": [ + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks" + ], + "compilerPath": "/usr/bin/clang", + "compilerArgs": [ + "-L/opt/homebrew/opt/qt@6/lib" + ], + "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "macos-clang-arm64" + } + ], + "version": 4 +} +``` + +### CLion + +1. Open CLion +2. Choose `File->Open` +3. Navigate to the source folder +4. Go to settings `Ctrl+Alt+S` +5. Navigate to `Toolchains` in `Build, Execution, Deployment` + - Set the correct build tools ([see here](https://i.imgur.com/daFAdVe.png)) + - CMake: `cmake` (optional) + - Make: `make` (optional) + - C Compiler: `gcc` + - C++ Compiler: `g++` + - Debugger: `gdb` (optional) +6. Navigate to `CMake` in `Build, Execution, Deployment` + - Set `Build directory` to `build` +7. Navigate to `Edit Configurations` ([see here](https://i.imgur.com/fu53nc3.png)) + - Create a new configuration + - Name: `All` + - Target: `All targets` + - Choose the newly added configuration as default + +Now you should be able to build and test ProjT Launcher with the `Build` and `Run` buttons. + +### Qt Creator + +1. Install Qt Creator within MSYS2 using `pacboy -S qt-creator:p` if building with it, otherwise you may use the regular installer or your package manager of choice (e.g. scoop) + - NOTE: If you install or run Qt Creator outside of MSYS2 when compiling through it, Qt Creator will fail to find the compiler. +2. (Optional) Create a shortcut to `C:\msys64\(msystem).exe qtcreator` + - Replace `(msystem)` with the msystem you're using (e.g. clang64). +3. Open Qt Creator (with the `qtcreator` command in MSYS2). +4. Choose `File->Open File or Project`. +5. Navigate to the Launcher source folder you cloned and choose `CMakeLists.txt`. +6. When prompted to configure the project, scroll past the **many** Desktop Qt options without changing anything and click "Configure Project" at the bottom right. +7. Cross your fingers, and press the "Run" button (bottom left of Qt Creator). + + - If the project builds successfully it will run and the Launcher window will pop up. diff --git a/archived/projt-launcher/docs/handbook/wiki/development/translating.md b/archived/projt-launcher/docs/handbook/wiki/development/translating.md new file mode 100644 index 0000000000..e56a74276d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/development/translating.md @@ -0,0 +1,16 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Translating ProjT Launcher + +The translation effort for ProjT Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/projtlauncher/launcher/), and information about translating ProjT Launcher is available at <https://github.com/Project-Tick/Translations> + +Current translation progress: +<div style="display: flex; justify-content: flex-end; max-width: 100%; overflow: hidden; margin-bottom: 1rem;"> + <a href="https://hosted.weblate.org/engage/projtlauncher/"> + <img + src="https://hosted.weblate.org/widgets/projtlauncher/-/launcher/multi-auto.svg" + alt="Translation status" + style="max-width: 100%; height: auto;" + /> + </a> +</div> diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/catpacks.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/catpacks.md new file mode 100644 index 0000000000..bebb0569f6 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/catpacks.md @@ -0,0 +1,121 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# CatPacks + +## Installation + +The CatPacks folder location is as follows: + +- on **Windows:** `%appdata%\ProjTLauncher\catpacks` +- on **Mac:** `~/Library/Application Support/ProjTLauncher/catpacks` +- on **Linux:** `~/.local/share/ProjTLauncher/catpacks` +- on **Flatpak:** `~/.var/app/org.projecttick.ProjTLauncher/data/ProjTLauncher/catpacks` + +After you move the CatPack to the mentioned folder restart ProjT Launcher, then open the global settings. +In the Launcher section, click the User Interface tab, and under Cat, choose the newly added CatPack. +Click the Close button, and enjoy your CatPack. + +## CatPack Types + +### 1. Simple CatPack + +The most basic CatPack is a simple image(that's all). + +### 2. Advanced CatPack + +Is a folder that contains a `catpack.json` file and some images. + +For example, the structure of a CatPack is as follows: + +```text +?ë¶´??€ test_catpack + ?ì’‹??€ catpack.json + ?ì’‹??€ christmas.png + ?ì’‹??€ oneDay.png + ?ì’‹??€ maxwell.png + ?ì’‹??€ newyear.png + ?ë¶´??€ newyear2.png +``` + +The `catpack.json` looks as follows: + +```json +{ + "name": "My Cute Cat", + "default": "maxwell.png", + "variants": [ + { + "startTime": { + "day": 12, + "month": 4 + }, + "endTime": { + "day": 12, + "month": 4 + }, + "path": "oneDay.png" + }, + { + "startTime": { + "day": 20, + "month": 12 + }, + "endTime": { + "day": 28, + "month": 12 + }, + "path": "christmas.png" + }, + { + "startTime": { + "day": 30, + "month": 12 + }, + "endTime": { + "day": 1, + "month": 1 + }, + "path": "newyear2.png" + }, + { + "startTime": { + "day": 28, + "month": 12 + }, + "endTime": { + "day": 3, + "month": 1 + }, + "path": "newyear.png" + } + ] +} +``` + +Fields description: + +- `name` the name of the CatPack +- `default` the path to the default cat +- `variants` a list of the cats that are visible only on specific days/periods. +- `startTime` the day from which the variant is visible (inclusive) +- `endTime` the day until the variant is visible (inclusive) +- `path` the path to the variant + +All paths are relative, so you only need to put the name of the image from the CatPack(with extension). + +The default cat is the cat displayed in all periods we can't match any variants. + +The variant matching is done by looking if today is between `startTime` and `endTime`, if is matched then that variant is displayed. + +The order of variants matters as the first variant that matches the period is returned ignoring the rest. + +So for the mentioned example, the variants are displayed as follows: + +- `oneDay.png` will be visible only on 12 April each year +- `christmas.png` from 20 December until 28 December +- `newyear.png` on 29 December +- `newyear2.png` from 30 December until 1 January +- `newyear.png` from 2 January until 3 January +- `maxwell.png` for the periods that were not mentioned + +So an example of order matter is if the `newyear.png` and `newyear2.png` orders would be reversed then `newyear2.png` will never be displayed. diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/command-line-interface.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/command-line-interface.md new file mode 100644 index 0000000000..9b46bf854d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/command-line-interface.md @@ -0,0 +1,33 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Using ProjT Launcher via the Command Line + +```text +Usage: projtlauncher [-h] [-v] [-d <dir>] [-l <launch>] [-s <server>] [-a <profile>] [-I <import>] [--show <show>] [--alive] [--help-all] + +Options: + -d, --dir <directory> Use a custom path as application root (use '.' for current directory) + -l, --launch <instance> Launch the specified instance (by instance ID) + -s, --server <address> Join the specified server on launch (only valid in combination with --launch) + -a, --profile <profile> Use the account specified by its profile name (only valid in combination with --launch) + --alive Write a small 'live.check' file after the launcher starts + -I, --import <file> Import instance from specified zip (local path or URL) + --show <show> Opens the window for the specified instance (by instance ID) + -h, --help Displays help on commandline options. + --help-all Displays help, including generic Qt options. + -v, --version Displays version information. +``` + +## What is an instance ID, and where do I find it? + +The instance ID is the name of the folder where your instance is contained. + +To find it, **right-click** on the instance you want to know the ID of, and then click on _Instance Folder_ within the context menu. + +Now, just copy the name of the folder that opened. + +## ProjT Launcher is still opening after I close Minecraft + +Currently, the _Close ProjT Launcher after game window opens_ option opens ProjT Launcher after closing Minecraft, even when launched from the CLI. + +To disable this, open _Settings_, then select _Minecraft_, and finally, un-check _Close ProjT Launcher after game window opens_ or check _Quit the launcher after game window closes_. diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/controller-support.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/controller-support.md new file mode 100644 index 0000000000..0a1a7cc9fd --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/controller-support.md @@ -0,0 +1,52 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Making use of Controllers + +This includes Steam Deck's Gamepad, PlayStation/Xbox controllers and so on. + +## Before proceeding + +* Please ensure that you are comfortable with the mod installation process within ProjT Launcher. +* A full guide on this can be [found here](../download-mods/). +* Not all game versions are supported at the time of writing, including those before 1.12.2. +* Please ensure a compatible version of Java is selected. [You can learn how to do so here](../installing-java/). + +Finally, if you don't have one already, you must create an **instance** with the game version and mod loader of your choice. + +## <img src="https://raw.githubusercontent.com/FabricMC/community/main/media/unascribed/png/fabric.png" alt="FabricMC Logo" height="20"><img src="https://raw.githubusercontent.com/QuiltMC/art/master/brand/svg/quilt_logo_dark.svg" alt="QuiltMC Logo" height="20"> Fabric/Quilt + +Before you continue, ensure that the correct version of the **Fabric API** mod on **Fabric** or the **Quilted Fabric API** mod on **Quilt** for your instance is installed. + +### <img src="https://cdn.modrinth.com/data/DOUdJVEm/4f8cdb3933f9efa0c5dfd5574d3ad6b101c7f3ef.png" alt="Controlify Logo" height="20"> Controlify (for Minecraft Versions 1.19.4 or newer) + +For Minecraft 1.19.4 or newer, we recommend [**Controlify**](https://modrinth.com/mod/controlify). It's a new controller mod that has [a lot of features that aren't in MidnightControls](https://github.com/isXander/Controlify/blob/1.20.x/dev/mod-comparison.md). + +It can be installed using ProjT Launcher's mod downloader function through either Modrinth (recommended) or CurseForge. Once installed, please launch your instance and navigate to the in-game controller menu (Options -> Controls -> Controller Settings) and do the calibration. Once that's done, you're ready to use your controller! + +### <img src="https://cdn-raw.modrinth.com/data/bXX9h73M/icon.svg" alt="MidnightControls Logo" height="20"> MidnightControls (for Minecraft Versions 1.18 or newer) + +For Minecraft 1.18 or newer, we recommend [**MidnightControls**](https://modrinth.com/mod/midnightcontrols), an updated fork of LambdaControls. + +It can be installed using ProjT Launcher's mod downloader function through either Modrinth (recommended) or CurseForge. Once installed, please launch your instance and navigate to the in-game controls menu. Within the in-game controls menu, you may need to change the "Mode" setting to **Controller** in order for the game to respond to input from the gamepad. If it doesn't work, you can use [**the app linked in the mod**](https://generalarcade.com/gamepadtool/) to edit the controller mappings. + +### <img src="https://cdn-raw.modrinth.com/data/W1D3UXEc/icon.png" alt="LambdaControls Logo" height="20"> LambdaControls (for Minecraft Versions 1.16.2 to 1.18) + +**NOTE:** This mod is currently unmaintained and hasn't been updated since June of 2021, so unless you really need to, use MidnightControls. + +For Minecraft 1.16.2 to 1.18, we recommend [**LambdaControls**](https://modrinth.com/mod/lambdacontrols). + +It can be installed using ProjT Launcher's mod downloader function through either Modrinth (recommended) or CurseForge. Once installed, please launch your instance and navigate to the in-game controls menu. Within the in-game controls menu, you may need to change the "Mode" setting to **Controller** in order for the game to respond to input from the gamepad. If it doesn't work, you can use [**the app linked in the mod**](https://generalarcade.com/gamepadtool/) to edit the controller mappings. + +### <img src="https://raw.githubusercontent.com/intergrav/Branding/main/deckcraft/mark/mark_svg.svg" alt="DeckCraft" height="20"> DeckCraft (for Minecraft versions 1.18.2 ??1.21.1) + +If you're on the Steam Deck, [DeckCraft](https://modrinth.com/project/deckcraft) might be useful to you. This is a modpack designed to make it easier to play Minecraft on the Deck. It includes MidnightControls, various optimizations, features, configurations, pre-installed shaders, and more. + +You can install it through the launcher by heading to `Add Instance`, select the `Modrinth` tab, then press `STEAM` + `X` together to open the keyboard and search for "DeckCraft". After this, select your preferred version and install it. + +## <img src="https://avatars0.githubusercontent.com/u/1390178?s=400&v=4" alt="Forge Logo" height="20"> Forge + +### <img src="https://raw.githubusercontent.com/MrCrayfish/Controllable/6caef1a4ac113e5c6ac1d1abde0f0cabc3e6ad97/src/main/resources/controllable_icon.png" alt="Controllable Logo" height="20"> Controllable (for Minecraft Versions 1.12.2 or newer) + +For Minecraft versions 1.12.2 or above, we also recommend [**Controllable**](https://www.curseforge.com/minecraft/mc-mods/controllable). + +Controllable can be installed using ProjT Launcher's mod downloader function through CurseForge. Once installed, you may now launch your instance, and find that the mod should begin working immediately. diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/create-instance.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/create-instance.md new file mode 100644 index 0000000000..e61d1451a7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/create-instance.md @@ -0,0 +1,13 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Creating an instance + +To create an instance, simply click on the **Add Instance** button on the top menu bar, or right-click an empty area in the middle of the launcher, and select **Create instance**. You can then either create a new instance by selecting your desired Minecraft version and modloader, or import an existing modpack by selecting a mod platform from the sidebar on the left. + +![Create Instance Shortcut](/projtlauncher/img/screenshots/projtlauncher_windows_dark_install_custom.png) + +## Editing an instance + +Once an instance is created, you can right-click it to open a context menu with some basic settings. From this list, select **Edit** to access the edit menu. + +![Console Window Version](/projtlauncher/img/screenshots/projtlauncher_windows_dark_console_window_version.png) diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/data-location.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/data-location.md new file mode 100644 index 0000000000..9ca656e218 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/data-location.md @@ -0,0 +1,32 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Data Location + +ProjT Launcher stores your data in your OS's standard application data folder. For the portable version, data is stored within its own directory. To find it, select **Folders** > **Launcher Root** in ProjT. Below is a table containing the default locations for each OS: + +| OS | Folder | +| -------------------------- | --------------------------------------------------------------- | +| Portable (Windows / Linux) | In the ProjT-Launcher folder | +| Windows | `%APPDATA%/ProjTLauncher` | +| Scoop | `%HOMEPATH%\scoop\persist\projtlauncher` | +| macOS | `~/Library/Application Support/ProjTLauncher` | +| Linux | `~/.local/share/ProjTLauncher` | +| Flatpak | `~/.var/app/org.projecttick.ProjTLauncher/data/ProjTLauncher` | + +## Internal folder structure + +| Folder | Purpose | +| ------------ | -------------------------------------------------------------- | +| assets | Stores the game files. | +| cache | Stores cached downloads. | +| catpacks | Stores the [catpacks](../catpacks). | +| icons | Stores instance icons. (default) | +| iconthemes | Stores launcher [icons themes](../change-themes#icons-pack). | +| instances | Stores user instances. (default) | +| java | Stores the java instalations managed by the launcher. | +| libraries | Stores libraries used to run Minecraft and Mod Loaders. | +| logs | Stores the logs. | +| meta | Stores the cached metadata information. | +| skins | Stores the player skins. | +| themes | Stores [themes](../change-themes). | +| translations | Stores GUI translations. | diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/download-modpacks.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/download-modpacks.md new file mode 100644 index 0000000000..fbdb79b3af --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/download-modpacks.md @@ -0,0 +1,42 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Downloading Modpacks + +Similar to [creating an instance](../create-instance), downloading a modpack can be done through the **Add Instance** menu. Or alternatively, *right click > create instance* +You can now use the sidebar to the left of the window to choose from the following: + +* Import +* ATLauncher +* CurseForge +* FTB Legacy +* FTB App Import +* Modrinth +* Technic + +After selecting which service you wish to use, you can browse the modpacks which available to install. + +The "**Import**" option assumes that you have manually downloaded a modpack to your computer through other means, and will ask for a **.zip** or **.mrpack** archive containing a ProjT Launcher compatible pack. + +The **FTB App Import** option assumes that you have **FTB App** installed on your PC, and have the modpack installed in FTB App. + +## Screenshots + +### CurseForge + +![CurseForge Modpacks](/projtlauncher/img/screenshots/projtlauncher_windows_dark_install_modpacks_curseforge.png) + +### Modrinth + +![Modrinth Modpacks](/projtlauncher/img/screenshots/projtlauncher_windows_dark_install_modpacks_modrinth.png) + +### Technic + +![Technic Modpacks](/projtlauncher/img/screenshots/projtlauncher_windows_dark_install_modpacks_technic.png) + +### Import + +![Import Modpack](/projtlauncher/img/screenshots/projtlauncher_windows_dark_install_import.png) + +### Custom + +![Custom Install](/projtlauncher/img/screenshots/projtlauncher_windows_dark_install_custom.png) diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/download-mods.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/download-mods.md new file mode 100644 index 0000000000..72d939111e --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/download-mods.md @@ -0,0 +1,19 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Downloading Mods + +Make sure you know how to [Create an Instance](../create-instance) before attempting to download mods. + +Inside the **Edit** menu, select the **Version** tab on the left sidebar. From here, you will need to decide on which mod loader you would like to use. **Fabric** is available for all versions starting with **1.14**. If you are using Forge mods, for Minecraft versions **1.20.1** and below, select **Forge**; for versions above **1.20.1**, it's generally recommended to use **NeoForge** instead. Content-heavy mods are typically made for **Forge** or **NeoForge**, while **Fabric** mods focus more on performance and QOL features. + +Once you have picked out your ideal mod loader, go to the **Mods** tab, and select the **Download Mods** option that's to the right. From here, you can choose to install from both *Modrinth* and *CurseForge* services. Modrinth focuses more towards free and open-source mods and is generally recommended, however, some older mods may only exist on CurseForge. + +It is worth noting that many mods will be available on **both platforms**. + +After you've chosen your mod provider, you can search or browse for any desired mods. Click the **Select mod for download** button to add each mod to your download queue. Once finished, you can now press **OK**, and your mods should begin downloading. + +## <img src="https://raw.githubusercontent.com/FabricMC/community/main/media/unascribed/png/fabric.png" alt="FabricMC Logo" height="20"> Fabric + +If you have elected to use the **Fabric** mod loader, then please ensure that the latest version of the **Fabric API** mod available for your game version is installed. It is mostly **required** for Fabric mods. + +If it is missing, you can install it from **Edit** > **Mods** > **Download mods** like any other mod. diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/index.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/index.md new file mode 100644 index 0000000000..b9f64caf1d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/index.md @@ -0,0 +1,13 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Getting Started + +How to set up and use ProjT Launcher to its fullest! + +![ProjT Launcher Main Window](/projtlauncher/img/screenshots/projtlauncher_windows_dark_main_window.png) + +## News + +![News](/projtlauncher/img/screenshots/projtlauncher_windows_dark_news.png) + +![More News](/projtlauncher/img/screenshots/projtlauncher_windows_dark_more_news.png) diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/install-of-alternatives.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/install-of-alternatives.md new file mode 100644 index 0000000000..4dcaaec125 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/install-of-alternatives.md @@ -0,0 +1,119 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Why bother? + +For older Minecraft versions, OptiFine was *the* way to make your game better, faster, and stronger through various useful features and optimizations. However, since Minecraft 1.16 there exist much better alternatives. + +## Detailed explanation + +For a very long time, OptiFine was an essential mod that many enjoyed using as it offers important benefits such as performance improvements, extra quality of life features, and more. However, it also has its downsides: + +- OptiFine is closed-source. This means that no one can easily inspect its code, and it is much more difficult for developers to make their mods compatible with OptiFine - since they can't see what could be conflicting between their mod and OptiFine. + +- OptiFine sometimes takes a long time to update to newer Minecraft versions as there is only one developer working on the mod. + +- OptiFine functions as an "all-in-one" mod (similar to a modpack). This makes it impossible for the user to disable/fully remove features that may be incompatible with other mods or that they don't need. + +In the past few years, various mods have been made to replace these features and offer a better experience for both mod developers *and* users. Most of them are open-source, updated faster due to community contributions and support, and allow you to remove features if they're incompatible with another mod or don't suit your preference. These will usually be available on Fabric and Quilt, with some of them natively supporting (or working through compatibility layers) Forge and/or NeoForge. + +## Installing things + +To install mods and modpacks, see the [Download mods](../download-mods) and [Download modpacks](../download-modpacks) pages. + +## <img src="https://raw.githubusercontent.com/FabricMC/community/main/media/unascribed/png/fabric.png" alt="FabricMC Logo" height="30"><img src="https://raw.githubusercontent.com/QuiltMC/art/master/brand/svg/quilt_logo_dark.svg" alt="QuiltMC Logo" height="30"> Fabric/Quilt Mods + +<div class="notification type-info"> +Most Fabric mods require the <a href="https://modrinth.com/mod/fabric-api">Fabric API</a> mod in order to work, while most Quilt mods require <a href="https://modrinth.com/mod/qsl">Quilted Fabric API</a> to work. +</div> + +### <img src="https://cdn.modrinth.com/data/AANobbMI/icon.png" alt="Sodium Logo" height="20"> Sodium + +[Sodium](https://modrinth.com/mod/sodium) is a mod that greatly improves render performance through various rendering optimizations. We **highly recommend** installing it when possible. + +If you use Sodium often, please consider supporting development of the mod by [donating](https://jellysquid.me/donate) to the developer! + +### <img src="https://cdn.modrinth.com/data/gvQqBUqZ/icon.png" alt="Lithium Logo" height="20"> Lithium + +[Lithium](https://modrinth.com/mod/lithium) is a mod that greatly improves render performance through various game logic optimizations. We **highly recommend** installing it when possible. + +### <img src="https://raw.githubusercontent.com/IrisShaders/Iris/trunk/src/main/resources/assets/iris/iris-logo.png" alt="Iris Logo" height="20"> Iris + +[Iris](https://irisshaders.dev/) allows you to use OptiFine shaderpacks, while also running Sodium. It currently supports almost every shaderpack, [with some exceptions](https://github.com/IrisShaders/Iris/blob/trunk/docs/supportedshaders.md#shaders-that-do-not-work-on-iris). + +### <img src="https://cdn.modrinth.com/data/Orvt0mRa/icon.png" alt="Indium Logo" height="20"> Indium + +[Indium](https://modrinth.com/mod/indium) is an addon for older Sodium versions that provides support for the Fabric Rendering API. This is needed if you want to use Sodium on Minecraft 1.20 or older with mods that use advanced rendering techniques. + +### <img src="https://cdn.modrinth.com/data/gvQqBUqZ/icon.png" alt="Lithium Logo" height="20"><img src="https://cdn.modrinth.com/data/H8CaAYZC/icon.png" alt="Starlight Logo" height="20"> Other Mods + +If you would like to go a bit further, LambdAurora maintains a [very detailed list of OptiFine alternatives for Fabric and Quilt](https://lambdaurora.dev/optifine_alternatives/). There's also [a list of mods used by the Additive modpack](https://github.com/skywardmc/additive/wiki/Give-up-OptiFine), which provides the Minecraft versions that each mod supports along with some extra information. + +## <img src="https://raw.githubusercontent.com/FabricMC/community/main/media/unascribed/png/fabric.png" alt="FabricMC Logo" height="30"><img src="https://raw.githubusercontent.com/QuiltMC/art/master/brand/svg/quilt_logo_dark.svg" alt="QuiltMC Logo" height="30"> Fabric/Quilt Modpacks + +### <img src="https://avatars.githubusercontent.com/u/92206402?s=200&v=4" alt="Fabulously Optimized Logo" height="20"> Fabulously Optimized + +If you don't want to search and install these mods manually, then try the [Fabulously Optimized](https://modrinth.com/modpack/fabulously-optimized) modpack, which supports almost all OptiFine features. + +[See the installation instructions](https://fabulously-optimized.gitbook.io/modpack/readme/install-instructions#prism-launcher) for ProjT Launcher. + +### <img src="https://cdn.modrinth.com/data/BYfVnHa7/icon.png" alt="Simply Optimized Logo" height="20"> Simply Optimized + +[Simply Optimized](https://modrinth.com/modpack/sop) is a modpack designed with just optimization in mind. SO has better out-of-the-box performance than Fabulously Optimized, but it doesn't come with the QoL mods or full OptiFine parity you would see in Fabulously Optimized, so you're expected to add any additional mods you want yourself. + +### <img src="https://raw.githubusercontent.com/skywardmc/art/main/additive/logo_512h.png" alt="Additive Logo" height="20"> Additive + +[Additive](https://modrinth.com/modpack/additive) is a modpack similar to Fabulously Optimized which aims to support nearly all OptiFine features. It's based on Adrenaline for better performance, supports Fabric, and can be installed on a wide range of Minecraft versions. + +### <img src="https://raw.githubusercontent.com/skywardmc/art/main/adrenaline/logo_512h.png" alt="Adrenaline Logo" height="20"> Adrenaline + +[Adrenaline](https://modrinth.com/modpack/adrenaline) is a Fabric modpack which aims to improve performance as much as possible while not changing anything about the vanilla game and not introducing instability. Like Simply Optimized, it does not come with any OptiFine replacement mods, so you'll have to manually install the features that you want. + +## <img src="https://avatars0.githubusercontent.com/u/1390178?s=400&v=4" alt="Forge Logo" height="30"> Forge/NeoForge Mods + +### <img src="https://cdn.modrinth.com/data/AANobbMI/icon.png" alt="Sodium Logo" height="20"> Sodium for Forge + +[Sodium](https://modrinth.com/mod/sodium) also exists for some Minecraft versions on Forge/NeoForge ??see [its support policy](https://github.com/CaffeineMC/sodium/wiki/Support-Policy). We highly recommend installing it when possible. + +### <img src="https://cdn.modrinth.com/data/gvQqBUqZ/icon.png" alt="Lithium Logo" height="20"> Lithium for Forge + +[Lithium](https://modrinth.com/mod/lithium) also exists for some Minecraft versions on Forge/NeoForge ??see [its support policy](https://github.com/CaffeineMC/lithium/wiki/Support-Policy). We highly recommend installing it when possible. + +### <img src="https://raw.githubusercontent.com/IrisShaders/Iris/trunk/src/main/resources/assets/iris/iris-logo.png" alt="Iris Logo" height="20"> Iris for Forge + +[Iris](https://irisshaders.dev/) also generally exists for the same loaders/versions as Sodium. + +<div class="notification type-warn"> +<strong>On older Minecraft versions with Forge, you'll need to use unofficial ports of OptiFine alternatives, which might have some compatibility/stability issues.</strong> + +While on some modpacks/with some mods they're going to work good, with others they might be <em>very</em> unstable so be well aware of what you're doing. If you have issues, use OptiFine. + +You can use <a href="https://modrinth.com/mod/connector">Sinytra Connector</a> to run many Fabric mods on Forge. + +<strong>Don't report issues with those on upstream's Discord support channels!</strong> +</div> + +### <img src="https://raw.githubusercontent.com/FiniteReality/embeddium/aa6657df4eaea8bdfa6243233c893207f5b7f8b4/src/main/resources/icon.png" alt="Embeddium" height="20"> Embeddium + +[Embeddium](https://modrinth.com/mod/embeddium) is a Sodium port for Forge that focuses on compatibility with other Forge mods. We generally recommend using it instead of Rubidium. Be aware of the statements above before using it. + +### <img src="https://raw.githubusercontent.com/Asek3/Oculus/1.18.2/src/main/resources/oculus-logo.png" alt="Oculus Logo" height="20"> Oculus + +[Oculus](https://modrinth.com/mod/oculus) is an Iris port for Forge. Be aware of the statements above before using it. + +### Radium + +[Radium](https://modrinth.com/mod/radium) is a Lithium port for Forge. Be aware of the statements above before using it. + +### Other mods + +You can find [a list of performance mods for Forge here](https://github.com/NordicGamerFE/usefulmods#performance-and-bug-fixing-mods). + +## <img src="https://avatars0.githubusercontent.com/u/1390178?s=400&v=4" alt="Forge Logo" height="30"> Forge Modpacks + +### <img src="https://raw.githubusercontent.com/skywardmc/art/main/hammer/logo_512h.png" alt="Hammer Logo" height="20"> Hammer + +[Hammer](https://modrinth.com/modpack/hammer) is like the Adrenaline modpack but made for Forge, utilizing Forge ports of mods and [Sinytra Connector](https://modrinth.com/mod/connector) to run Fabric/Quilt optimization mods. + +### <img src="https://raw.githubusercontent.com/skywardmc/art/main/drill/logo_512h.png" alt="Drill Logo" height="20"> Drill + +[Drill](https://modrinth.com/modpack/drill) is the Additive modpack but made for Forge, utilizing Forge ports of mods and [Sinytra Connector](https://modrinth.com/mod/connector) to run Fabric/Quilt optimization and OptiFine replacement mods. diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-java.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-java.md new file mode 100644 index 0000000000..f056e8dc14 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-java.md @@ -0,0 +1,179 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Installing Java + +## Automatically installing Java (recommended) + +In ProjT Launcher 0.0.1+, you can let the launcher handle Java automatically on Windows, macOS and Linux (not available in all packages) by enabling "Autodetect Java version" and "Auto-download Mojang Java" in: + +> Settings (top toolbar) > Java + +This will make the launcher automatically download the right version of Java for your version of Minecraft (Java 8 for pre-1.17, Java 17 for 1.17-1.20.4, etc.). The rest of this page is going to explain downloading Java manually, and is probably not needed for you. + +If the Java downloader is not available for you - which is the case on most Linux packages apart from the official Flatpak and AppImage downloads - manual installation is recommended instead. + +## Manually installing Java + +Oracle Java is not recommended. As of 2019, it has licensing that prohibits certain uses, and it requires an account to download certain versions. Their downloads are intended for those who have purchased a support contract from Oracle. + +Instead, this guide recommends installing **OpenJDK** from one of the reputable vendors. OpenJDK is a source code only distribution of Java made by a collaboration between Oracle and industry partners such as Adoptium, Microsoft, Red Hat, and Azul. Unlike Oracle Java, it is released under a license that minimizes legal risk for users and does not require payment. + +If you would like to learn more about OpenJDK and the vendors mentioned in this page, here are their websites: + +- [Adoptium Temurin](https://adoptium.net/) +- [Microsoft OpenJDK](https://www.microsoft.com/openjdk) +- [Azul OpenJDK](https://www.azul.com/downloads/) +- [OpenJDK source distribution](https://openjdk.org/) + +Otherwise, continue reading for quick download links and a guide for which one to choose. + +### Selecting Java + +Once you have **installed** Java, ProjT Launcher will be able to detect it during the first time set-up wizard. + +If you installed Java after already completing the first time setup process, you can access and modify your Java configuration globally for all instances through: + +> Settings > Java > Java Runtime > Auto-Detect... + +or for a specific instance: + +> Right click an instance > Edit > Settings > Java > Java installation > Auto-Detect... + +If you have just installed a version of Java and it doesn't show up, try pressing Refresh or restart the launcher. If you don't have your desired Java version installed, keep reading. + +### Installing Java on Windows + +**First, check your CPU architecture.** Here is a method that works on all Windows versions: + +1. Click or tap on the **Start Menu** +2. Search **Command Prompt** or **Terminal** and click on it +3. Type in `echo %PROCESSOR_ARCHITECTURE%` and press enter +4. Read the result. + - If it says `AMD64`, you have a 64-bit **x86-64** CPU. This is sometimes called **x64** or **amd64**. + - If it says `X86`, you have a 32-bit **x86** CPU. This is sometimes called **x32** or **x86-32**. + - If it says `ARM64`, you have a 64-bit **ARM** CPU. This is sometimes called **aarch64** or **ARM64**. + +Then, download the appropriate Java: + +| Minecraft | CPU type | Download page | Viable alternatives | +| :---: | :---: | -- | --- | +| Minecraft **1.20.5** or above | x86-64 | [Microsoft OpenJDK 21 for Windows x64, `.msi` installer](https://aka.ms/download-jdk/microsoft-jdk-21-windows-x64.msi) | Azul, Coretto, Temurin, GraalVM | +| | x86 | Not available :( | | +| | aarch64 | [Microsoft OpenJDK 21 for Windows aarch64, `.msi` installer](https://aka.ms/download-jdk/microsoft-jdk-21-windows-aarch64.msi) | Azul | +| Minecraft **1.17** to **1.20.4** | x86-64 | [Microsoft OpenJDK 17 for Windows x64, `.msi` installer](https://aka.ms/download-jdk/microsoft-jdk-17-windows-x64.msi) | Azul, Coretto, Temurin, GraalVM | +| | x86 | [Temurin OpenJDK 17 for Windows x32, `.msi` installer](https://adoptium.net/temurin/releases/?version=17&arch=x86&os=windows) | Azul, Coretto | +| | aarch64 | [Microsoft OpenJDK 17 for Windows aarch64, `.msi` installer](https://aka.ms/download-jdk/microsoft-jdk-17-windows-aarch64.msi) | Azul | +| Minecraft **1.16** or below | x86-64 | [Temurin OpenJDK 8 for Windows x64, `.msi` installer](https://adoptium.net/temurin/releases/?version=8&arch=x64&os=windows) | Azul, Coretto | +| | x86 | [Temurin OpenJDK 8 for Windows x64, `.msi` installer](https://adoptium.net/temurin/releases/?version=8&arch=x86&os=windows) | Azul, Coretto | +| | aarch64 | Not available :( | | + +**Tip:** If you are on Windows 11, you can automatically install 8, 17 and 21 by running `winget install Microsoft.OpenJDK.21; winget install Microsoft.OpenJDK.17; winget install EclipseAdoptium.Temurin.8.JDK` in the Terminal app. + +### Installing Java on macOS + +**First, check your CPU architecture.** + +1. Open the Apple menu in the top left corner of the screen +2. Click "About This Mac" +3. Check what it says after "Processor" + - If it contains `Intel`, you have a 64-bit **x86-64** CPU. This is sometimes called **x64** or **amd64**. + - If it contains `Apple`, you have a 64-bit **ARM** CPU. This is sometimes called **aarch64** or **ARM64**. + +Then, download the appropriate Java: + +| Minecraft | CPU type | Download page | Viable alternatives | +| :---: | :---: | --- | --- | +| Minecraft **1.20.5** or above | x86-64 | [Microsoft OpenJDK 21 for macOS x64, `.pkg` installer](https://learn.microsoft.com/en-us/java/openjdk/download) | Azul, Coretto, Temurin, GraalVM | +| | aarch64 | [Microsoft OpenJDK 21 for macOS aarch64, `.pkg` installer](https://learn.microsoft.com/en-us/java/openjdk/download) | Azul, Coretto | +| Minecraft **1.17** to **1.20.4** | x86-64 | [Microsoft OpenJDK 17 for macOS x64, `.pkg` installer](https://learn.microsoft.com/en-us/java/openjdk/download) | Azul, Coretto, Temurin, GraalVM | +| | aarch64 | [Microsoft OpenJDK 17 for macOS aarch64, `.pkg` installer](https://learn.microsoft.com/en-us/java/openjdk/download) | Azul, Coretto | +| Minecraft **1.16** or below | x86-64 | [Temurin OpenJDK 8 for macOS x64, `.pkg` installer](https://adoptium.net/temurin/releases/?version=8) | Azul, Coretto | +| | aarch64 | [Azul OpenJDK 8 for macOS aarch64, `.dmg` installer](https://www.azul.com/downloads/?version=java-8-lts&os=macos&architecture=arm-64-bit&package=jdk) | Coretto | + +### Installing Java on Linux + +On Linux, it's recommended to use your package manager for installing Java. + +#### Fedora, RHEL, CentOS, AlmaLinux, or RockyLinux + +On the COPR package all required Java versions should be installed, but this is the command to install it: + +```bash +sudo dnf install java-1.8.0-openjdk java-17-openjdk java-21-openjdk +``` + +#### Void Linux + +```bash +sudo xbps-install openjdk17-jre openjdk8-jre openjdk21-jre +``` + +#### Arch Linux, Manjaro, EndeavorOS, Garuda + +```bash +sudo pacman -S jre17-openjdk jre8-openjdk jre21-openjdk +``` + +#### Ubuntu, Pop!\_OS, Linux Mint, Zorin OS, or elementaryOS + +```bash +sudo apt install openjdk-17-jre openjdk-8-jre openjdk-21-jre +``` + +#### Debian, MX Linux + +```bash +sudo apt install openjdk-17-jre openjdk-21-jre +``` + +Java 8 is not available in Debian 10+ due to lack of security support, but you can use the Adoptium repository for security support until 2026: + +```bash +wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add - +sudo add-apt-repository --yes https://packages.adoptium.net/artifactory/deb/ +sudo apt-get update && sudo apt-get install temurin-8-jdk +``` + +#### Alpine Linux + +```bash +sudo apk add openjdk17 openjdk8 +``` + +#### Flatpak + +The ProjT Launcher Flatpak already bundles Java. + +#### NixOS + +The ProjT Launcher NixOS package already bundles Java. + +### Special cases + +#### Forge 1.16.5 and Java 8u321+ + +Old versions of Forge crash with Java 8u321+. For this reason, using Java 8u312 or lower is recommended. + +#### A note about Intel HD 2000/3000 on Windows 10 + +Since those iGPUs are not *officially* supported on Windows 10, with them the game is likely going to crash with any modern java binary. + +<!-- markdownlint-disable-next-line link-fragments --> +*For 1.16.5 or older* there's a workaround in installing an older Java binary. Mojang Java 8u51 is recommended, and can be automatically installed with the [above](#automatically-installing-java-(recommended)). + +Unfortunately there's no workaround for Java 17 (and so newer Minecraft), so you can only downgrade your Windows or switch to Linux there. +If you want to try, [there's this guide that could work](https://gist.github.com/rb-dahlb/26f316c5b6089807a139fc44ee69f0d1). Nothing is guaranteed here, though. + +#### Older Minecraft on MacOS + +If you use some older Minecraft versions, you might have had this error: + +```text +Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!' +``` + +Here is a fix: + +- First, remove, if you had, [your current Oracle Java](https://explainjava.com/uninstall-java-macos/) +- Then download and install [this Java 8u241 binary](https://files.multimc.org/downloads/jre-8u241-macosx-x64.dmg) +- Select this java binary on ProjT Launcher, and it should fix your issue! diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-optifine.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-optifine.md new file mode 100644 index 0000000000..c0da206bb6 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-optifine.md @@ -0,0 +1,66 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Before you continue + +Because OptiFine is an old, closed-source project, it has evolved to cause many mod incompatibilities, and doesn't do as good of a job as some the alternatives out there. + +Also, if you're using the Fabric mod loader, there's no native support for it, and the current workaround is known to be buggy and quite unstable, due to the very nature of OptiFine and OptiFabric. + +Therefore, you should consider the use of OptiFine alternatives whenever possible. [See the wiki page about it](../install-of-alternatives). + +## <img src="https://www.optifine.net/favicon.ico" alt="OptiFine Logo" height="20" /> Installing OptiFine standalone + +This method requires you either have the Minecraft version you wish to install OptiFine on to be installed through the vanilla launcher or to recreate the folder structure somewhere on your system (like /.minecraft/versions/*1.18.2*/*1.18.2*.jar). + +Go to <https://optifine.net/downloads> and find your respective Minecraft version (eg. 1.18.2). + +Once found, click **mirror**. Now click the **download** button to download your OptiFine jar file. + +Execute the OptiFine jar file and change the selected folder location if required, then hit the **"Extract"** button and save the jarmod file somewhere you'll remember. + +Now in ProjT Launcher edit the instance you wish to install OptiFine on, open the **Version** tab and click **Add to Minecraft.jar**, select the extracted OptiFine jarmod (the file ending in \_MOD.jar) and confirm. + +OptiFine will now be installed as a jarmod in that instance, so you can remove the installer and extracted jarmod. + +From OptiFine Version **H1_pre2** the instructions for installing OptiFine on ProjT Launcher have changed. Make sure to have the version of Minecraft that you wish to install OptiFine on installed through the vanilla launcher. + +1. Extract the MOD jar with the OptiFine installer +2. Open it with any archiver +3. Move all files from the folder called **notch** to the top folder of the archive file and click **save**. +4. Open ProjT Launcher, edit the instance you wish to install OptiFine on, open the **Version** tab and click **Add to Minecraft.jar**, select the extracted / modified OptiFine jarmod (the file ending in _MOD.jar) and confirm. + +## <img src="https://www.optifine.net/favicon.ico" alt="OptiFine Logo" height="20" /> Installing OptiFine on top of a modloader + +Make sure you know how to [download mods](../download-mods) before attempting to install OptiFine. + +Go to <https://optifine.net/downloads> and find your respective Minecraft version (eg. 1.18.2). + +Once found, click **mirror**. Now click the **download** button to download your OptiFine jar file. + +Remember where you have kept your **.jar** file, and continue to ProjT Launcher. Follow the steps from the [download mods](../download-mods) page, and choose either Forge or Fabric. + +### <img src="https://avatars0.githubusercontent.com/u/1390178?s=400&v=4" alt="Forge Logo" height="20"> Forge + +Forge does not require any extra steps besides adding the **.jar** for OptiFine into ProjT Launcher. + +**NOTE:** Some versions of OptiFine **don't** work on Forge! + +### <img src="https://raw.githubusercontent.com/FabricMC/community/main/media/unascribed/png/fabric.png" alt="FabricMC Logo" height="20"> Fabric + +**Note:** If you're playing on Minecraft versions older than 1.16, you might need to also install the [Fabric API](../download-mods/#fabric) mod. + +Fabric, unlike Forge, does not natively support OptiFine, and will require the installation of OptiFabric. + +Go into the **Mods** tab on the left side and then in the right menu select **Download mods**, then select *CurseForge* and search *OptiFabric*. Once found, click **Select mod for download**, and then press **OK**. You may now proceed with adding the .jar file for OptiFine into ProjT Launcher. + +If there were results shown in the search, your Minecraft version may not be compatible with OptiFabric. In this case, you can either try the Forge method, or choose to wait until support for your Minecraft version is added. + +### <img src="https://raw.githubusercontent.com/QuiltMC/art/master/brand/svg/quilt_logo_dark.svg" alt="QuiltMC Logo" height="20"> Quilt + +There's no way of running OptiFine on Quilt at the time of writing. + +### Adding the .jar file into ProjT Launcher + +Go into the **Mods** tab on the left side of the **Edit Instance** menu, and then click on the **Add .jar** option to the right of the window. + +Now, find and select the **.jar** file you downloaded earlier, and it should now be added to your instance. Enjoy! diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-projtlauncher.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-projtlauncher.md new file mode 100644 index 0000000000..a8bc7260bb --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/installing-projtlauncher.md @@ -0,0 +1,5 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# <img src="https://raw.githubusercontent.com/Project-Tick/ProjT-Launcher/develop/program_info/org.projecttick.ProjTLauncher.svg" alt="ProjT Launcher Logo" height="20" /> Installing ProjT Launcher + +Installing ProjT Launcher on most platforms should be quite straightforward. Head over to our [Downloads](/projtlauncher/download/) page, and choose the option that'll work best for your operating system. diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/migrating-prismlauncher.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/migrating-prismlauncher.md new file mode 100644 index 0000000000..81a0df930f --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/migrating-prismlauncher.md @@ -0,0 +1,31 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Migrating instances from ProjT Launcher + +## <img src="https://avatars.githubusercontent.com/u/116031705" alt="Prism Launcher Logo" height="20" /> In Prism Launcher + +In order to transfer an instance, or multiple instances from Prism Launcher to ProjT Launcher, you must begin the process by opening Prism Launcher on your computer. + +Now that Prism Launcher is open, navigate to the **Folders** tab located on the top menu bar, to the left of the Settings tab. + +Left-click on the tab and select the **View Instance Folder** option. This should then open your system's file explorer application with the correct directory in view. + +Now that the Prism Launcher instance directory is open, you may **select**, and then **copy** the instances that you wish to transfer. + +## <img src="https://raw.githubusercontent.com/Project-Tick/ProjT-Launcher/develop/program_info/org.projecttick.ProjTLauncher.svg" alt="Prism Launcher Logo" height="20" /> In ProjT Launcher + +To complete the transfer process, you must now open ProjT Launcher. + +As you did in Prism Launcher previously, navigate to the **Folders** tab located on the top menu bar, to the left of the Settings tab. + +Left-click on the tab and select the **Instances** option. This should once again open your system's file explorer application with the correct directory in view. + +Now that the ProjT Launcher instance directory is open, you may **paste** the instance(s) that you previously copied from the Prism Launcher directory. + +*Optional: If you would like to transfer your pack icon selections, navigate up from the Prism Launcher instances folder to the main Prism Launcher folder. Do the same for Prism Launcher. Copy the* **icons** *folder from Prism Launcher to Prism Launcher.* + +## Finishing up + +Now, in **ProjT Launcher**, your instances should appear as they normally would in Prism Launcher. + +If you have transferred instances across devices or packaging formats, you may need to change the version of **Java** that'll be used to launch Minecraft. You can learn how to do so on [this page](../installing-java). diff --git a/archived/projt-launcher/docs/handbook/wiki/getting-started/settings.md b/archived/projt-launcher/docs/handbook/wiki/getting-started/settings.md new file mode 100644 index 0000000000..9337471a13 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/getting-started/settings.md @@ -0,0 +1,49 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Settings + +ProjT Launcher has various settings to customize your experience. + +## Accounts + +![Accounts Settings](/projtlauncher/img/screenshots/projtlauncher_windows_dark_settings_accounts.png) + +## Appearance + +![Appearance Settings](/projtlauncher/img/screenshots/projtlauncher_windows_dark_settings_appearance.png) + +## General + +### Backups Opened + +![General Settings Backups Opened](/projtlauncher/img/screenshots/projtlauncher_windows_dark_settings_general_backups_closed.png) + +### Backups Closed + +![General Settings Backups Closed](/projtlauncher/img/screenshots/projtlauncher_windows_dark_settings_general_backups_opened.png) + +## Java + +![Java Settings](/projtlauncher/img/screenshots/projtlauncher_windows_dark_settings_java.png) + +## Language + +![Language Settings](/projtlauncher/img/screenshots/projtlauncher_windows_dark_settings_language.png) + +## Tools + +![Tools Settings](/projtlauncher/img/screenshots/projtlauncher_windows_dark_settings_tools.png) + +## Console + +### Backups + +![Console Windows Backups](/projtlauncher/img/screenshots/projtlauncher_windows_dark_console_windows_backups.png) + +### Minecraft Log + +![Console Windows Minecraft Log](/projtlauncher/img/screenshots/projtlauncher_windows_dark_console_windows_mclog.png) + +### Version + +![Console Window Version](/projtlauncher/img/screenshots/projtlauncher_windows_dark_console_window_version.png) diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/apis.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/apis.md new file mode 100644 index 0000000000..17ab081fab --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/apis.md @@ -0,0 +1,45 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# APIs + +## Services + +![Services tab under APIs tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_services.png) + +### Pastebin Service + +<!-- TODO: add image of Pastebin Service section --> + +This is where you can choose which Paste Service to use. + +### Metadata Server + +<!-- TODO: add image of Metadata Server section --> + +This is where you can set where the launcher gets it metadata. + +## API Keys + +![User API Keys tab under APIs tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_services.png) + +### Microsoft Authentication + +<!-- TODO: add image of Microsoft Authentication section --> + +Set this if you want to use your own client ID for Microsoft Authentication. + +### CurseForge Core API + +<!-- TODO: add image of CurseForge Core API section --> + +Set this if you want to use your own CurseForge API key. + +## Miscellaneous + +![Miscellaneous tab under APIs tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_services.png) + +### User Agent + +<!-- TODO: add image of User Agent section --> + +Set this if you want to use a custom User Agent. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/atl-platform.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/atl-platform.md new file mode 100644 index 0000000000..5628052d08 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/atl-platform.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# ATLauncher + +![Launcher tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_atlauncher.png) + +Here you can browse ATLauncher packs and install them. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/custom-commands.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/custom-commands.md new file mode 100644 index 0000000000..ec1497ba38 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/custom-commands.md @@ -0,0 +1,18 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Custom Commands + +![Custom Commands tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_general.png) + +Pre-launch command runs before the instance launches and post-exit command runs after it exits. + +Both will be run in the launcher's working folder with extra environment variables: + +- $INST_NAME - Name of the instance +- $INST_ID - ID of the instance (its folder name) +- $INST_DIR - absolute path of the instance +- $INST_MC_DIR - absolute path of Minecraft +- $INST_JAVA - Java binary used for launch +- $INST_JAVA_ARGS - command-line parameters used for launch (warning: will not work correctly if arguments contain spaces) + +Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux) diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/environment-variables.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/environment-variables.md new file mode 100644 index 0000000000..c2e7cdf171 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/environment-variables.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Environment Variables + +![Custom Commands tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_general.png) + +Here you can add environment variables to be provided to the java runtime when launching the game. This can be useful for setting up specific configurations or debugging options. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/flame-platform.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/flame-platform.md new file mode 100644 index 0000000000..d2024ae9c7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/flame-platform.md @@ -0,0 +1,9 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# CurseForge + +![CurseForge tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_curseforge.png) + +Here you can browse CurseForge packs and install them. + +Note: Some modpacks may require the downloading of certain components via browser. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/ftb-platform.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/ftb-platform.md new file mode 100644 index 0000000000..f8473f74bc --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/ftb-platform.md @@ -0,0 +1,9 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# FTB + +![FTB tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance.png) + +Here you can browse FTB packs and install them. + +Note: Some modpacks may require the downloading of certain components via browser. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/index.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/index.md new file mode 100644 index 0000000000..f187fb21d7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/index.md @@ -0,0 +1,41 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Help Pages + +Guides for ProjT Launcher settings, instance management, and mod platform importers. Content is adapted from the upstream Prism Launcher help pages with ProjT branding and local screenshots. + +## Settings + +- [Launcher Settings](./launcher-settings/) +- [Java Settings](./java-settings/) +- [Java Wizard](./java-wizard/) +- [Language Settings](./language-settings/) +- [Minecraft Settings](./minecraft-settings/) +- [Proxy Settings](./proxy-settings/) +- [Custom Commands](./custom-commands/) +- [Environment Variables](./environment-variables/) +- [APIs](./apis/) +- [External Tools](./tools/) +- [Notes](./notes/) + +## Instances + +- [Instance Version](./instance-version/) +- [Instance Copy](./instance-copy/) +- [Worlds](./worlds/) +- [Screenshots](./screenshots-management/) +- [Zip Import](./zip-import/) + +## Mods & Downloads + +- [Mods](./loader-mods/) +- [Mod Downloader](./mod-platform/) + +## Modpack Platforms + +- [Custom / Vanilla](./vanilla-platform/) +- [Modrinth](./modrinth-platform/) +- [CurseForge](./flame-platform/) +- [FTB](./ftb-platform/) +- [Technic](./technic-platform/) +- [ATLauncher](./atl-platform/) diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/instance-copy.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/instance-copy.md new file mode 100644 index 0000000000..6d7f674ab3 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/instance-copy.md @@ -0,0 +1,77 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Copy an Instance + +When you right click on an instance, you can choose to copy it. After selecting this option, you will see a new window. You can choose an icon, name and a group for the new instance. + +![Copy menu when right click on instance](/projtlauncher/img/screenshots/projtlauncher_dark_create_instance_shortcut.png) + +## Instance Copy Options + +This is where you can find options and copy settings for your new instance. + +### Keep play time + +This option will allow you to keep your playtime from your original instance, if this setting is enabled in Global Settings, see: [Game Time](../minecraft-settings/#game-time) + +### Copy saves + +This option allows you to copy all saved games from your original instance to your new one. + +### Copy game options + +This option allows you to copy all game settings, if you want your configuration to be cloned. + +### Copy resource packs + +This option allows you to copy all the resource packs to your new instance. + +### Copy screenshots + +This option allows you to copy all your screenshots to your new instance folder. + +### Copy shader packs + +This option allows you to copy all your shader packs to your new instance folder. + +### Copy servers + +This option allows you to copy all your servers to your new instance. You can find them in the multiplayer menu of your new instance. + +### Copy mods + +This option allows you to copy all your mods to your new instance folder. + +### Select all + +This option allows you to select all the precedent options. + +## Advanced Copy Options + +This is where you can set more advanced options, such as links or cloning. + +### Symbolic and Hard Link Options + +You can't use these options if your partition is on FAT filesystem. + +#### Use symbolic links + +This option will create a symbolic link between the two instances. + +#### Use hard links + +This option will create a hard link between the two instances. + +#### Link files recursively + +This option will link all files, instead of just the parent folder. + +#### Don't link saves + +This option will disable the link of the saves. The world saves will be copied instead. + +### CoW (Copy-on-Write) Options + +#### Clone instead of copying + +This option is only supported on APFS, BTRFS, BCACHEFS, REFS, XFS and ZFS filesystems. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/instance-version.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/instance-version.md new file mode 100644 index 0000000000..f8287bcb0c --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/instance-version.md @@ -0,0 +1,37 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Instance Version + +![Version tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_version.png) + +This page is for managing the core parts of the Instance. + +### Selection + +<!-- TODO: add image of Selection section --> + +Here you can change the version, change the load order and remove components. + +### Edit + +<!-- TODO: add image of Edit section --> + +This is for customizing the .json file that is used to load that component. + +### Install + +<!-- TODO: add image of Install section --> + +From here you can install loader mods. + +### Advanced + +<!-- TODO: add image of Advanced section --> + +This is for adding and replacing components that change the Minecraft.jar + +### Folder + +<!-- TODO: add image of Folder section --> + +This are shortcuts that open a specific folder. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/java-settings.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/java-settings.md new file mode 100644 index 0000000000..00154be71b --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/java-settings.md @@ -0,0 +1,42 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Java Settings + +### Java settings + +In this page you can set the global Java settings. + +![Java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +## Memory + +![Memory section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +Java memory settings. +NOTE: MORE RAM ALLOCATED DOESN'T MEAN BETTER PERFORMANCE! In fact, in most use cases (except you're playing big modpacks) 4GB of ram allocated should be more than enough + +## Java Runtime + +![Java Runtime section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +This is where the settings for the Java runtime live, like the location of the runtime and any Java arguments to use. + +For information about how to get a correct Java version, see: [Installing Java](../getting-started/installing-java/). + +**Auto-detect** will check your computer for all java versions and show you a list of them, the best one on top. + +**Test** can be used to test the selected Java runtime along with your memory settings and JVM arguments without starting the game. + +**Skip java compatibility checks** skips java compatibility checks at game launch + +**Autodetect Java version** (Recommended) sets the correct java version at game launch (only looks with managed (see below) java and will change the java path in instance settings) + +**Auto-download Mojang Java** (Recomnended) will automatically download needed the Java runtimes you need (needs the above setting to be on) + +## Java Management + +![Java Management section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +This is where you can download and remove the java that were installed by ProjT Launcher. + +![Java Download section of java tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/java-wizard.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/java-wizard.md new file mode 100644 index 0000000000..51a5f7a31b --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/java-wizard.md @@ -0,0 +1,13 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Java Wizard + +![Java wizard page displayed on the first launch](/projtlauncher/img/screenshots/projtlauncher_dark_settings_java.png) + +In this page you can set the global Java settings. + +For more information on how to manually download and install Java check the documentation [found here](../getting-started/installing-java/). + +For more information about each Java setting check the documentation [found here](../java-settings/). + +We recommend that new users enable both the Autodetect and Auto-download Java features, in order not to need to manually set up their java path for each instance. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/language-settings.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/language-settings.md new file mode 100644 index 0000000000..823773bd0d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/language-settings.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Language Settings + +![Language tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_language.png) + +In this Page, you can select the language ProjT Launcher should use. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/launcher-settings.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/launcher-settings.md new file mode 100644 index 0000000000..84f3982142 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/launcher-settings.md @@ -0,0 +1,63 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Launcher Settings + +## Features + +![Features tab under Launcher tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_general.png) + +### Folders + +<!-- TODO: add image of folder section --> + +This is where you can set the location for the Instances, Mods & Icons folders. + +### Mods + +<!-- TODO: add image of mods section --> + +Disable the usage of metadata provided by Modrinth and CurseForge for mods. + +## User Interface + +![User Interface tab under Launcher tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_appearance.png) + +### Instance view sorting mode + +<!-- TODO: add image of Instance view sorting mode section --> + +How the instances should be sorted. + +### Theme + +<!-- TODO: add image of Theme section --> + +This is where you can choose which Icon Theme, Color Theme or Background Cat you want. + +### Tools + +<!-- TODO: add image of Tools section --> + +This is where you can choose if you want a menubar or toolbar. + +## Console + +![Console tab under Launcher tab in settings](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_settings.png) + +### Console Settings + +<!-- TODO: add image of Console Settings section --> + +This is where you set when the log window should appear. + +### History limit + +<!-- TODO: add image of History limit section --> + +This is where you set if you want to limit the log length and by how much. + +### Console font + +<!-- TODO: add image of Console font section --> + +This is where you can change the font and font size of the log. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/loader-mods.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/loader-mods.md new file mode 100644 index 0000000000..1b4a23e1b6 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/loader-mods.md @@ -0,0 +1,11 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Mods + +![Mods tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_mods.png) + +This page reflects the contents of the instance's mods folder and is used for mods that loaders like Forge, Fabric, NeoForge, Quilt and LiteLoader can read and add to the game. Unlike an ordinary file explorer, it also shows details about the mods like version and name. + +You can drag and drop more mods into the view, just like any other folder. The mods also have a checkbox next to them, which allows you to temporarily disable them. + +Clicking "Download Mods" you can download mods from Modrinth and CurseForge. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/minecraft-settings.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/minecraft-settings.md new file mode 100644 index 0000000000..607820c866 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/minecraft-settings.md @@ -0,0 +1,37 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Minecraft Settings + +![Minecraft tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_minecraft.png) + +### Window Size + +<!-- TODO: add image of Window Size section --> + +This is where you set the Window size and if it should be maximized\*. + +\*Only works on 1.5.2 and older + +### Native library workarounds + +<!-- TODO: add image of Native library workarounds section --> + +Enable the usage of system libraries instead of the included ones. + +### Performance + +<!-- TODO: add image of Performance section --> + +Enable some external tools related to performance. + +### Game time + +<!-- TODO: add image of Game time section --> + +Set if you want to record play time and which to show. + +### Miscellaneous + +<!-- TODO: add image of Miscellaneous section --> + +Set when you want to close or quit the launcher. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/mod-platform.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/mod-platform.md new file mode 100644 index 0000000000..7219cb7b0a --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/mod-platform.md @@ -0,0 +1,17 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Mod Downloader + +![Mod downloader on mods tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_download_mods.png) + +In this page, you can download mods from Modrinth and CurseForge. + +On the left, you can choose the mod provider. + +Clicking "Select mod for Download" selects the mod to be downloaded. + +Clicking "Filter options" you can choose how strict the filters will be. + +Clicking "Cancel" returns to the previous page. + +Clicking "OK" allows you to see the mods you have decided to download and to download them. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/modrinth-platform.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/modrinth-platform.md new file mode 100644 index 0000000000..ad87016f0a --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/modrinth-platform.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Modrinth + +![Modrinth tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_modrinth.png) + +Here you can browse Modrinth packs and install them. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/notes.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/notes.md new file mode 100644 index 0000000000..0625623582 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/notes.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Notes + +![Notes tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_notes.png) + +Here you can write some notes about your instance in plain text. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/proxy-settings.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/proxy-settings.md new file mode 100644 index 0000000000..fb4e6d6959 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/proxy-settings.md @@ -0,0 +1,23 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Proxy Settings + +![Proxy tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_proxy.png) + +### Type + +<!-- TODO: add image of Type section --> + +This is where you choose if and what type of proxy you want to use. + +### Address and Port + +<!-- TODO: add image of Address and Port section --> + +This is where you can set the Address and Port of your proxy server. + +### Authentication + +<!-- TODO: add image of Authentication section --> + +This is where you fill in the login details of your proxy. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/screenshots-management.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/screenshots-management.md new file mode 100644 index 0000000000..06a5d091b7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/screenshots-management.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Screenshots + +![Screenshots tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_screenshots.png) + +Here you can rename, deleted, copy and upload you in game screenshots. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/technic-platform.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/technic-platform.md new file mode 100644 index 0000000000..8863f4fe58 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/technic-platform.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Technic + +![Technic tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_technic.png) + +Here you can browse Technic packs and install them. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/tools.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/tools.md new file mode 100644 index 0000000000..eaa65ad193 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/tools.md @@ -0,0 +1,31 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# External Tools + +![External Tools tab under ProjT Launcher settings](/projtlauncher/img/screenshots/projtlauncher_dark_settings_tools.png) + +<!-- TODO: add description of what each tool does --> + +### JProfiler + +<!-- TODO: add image of JProfiler section --> + +This is where you set to path to your JProfiler executable. + +### JVisualVM + +<!-- TODO: add image of JVisualVM section --> + +This is where you set to path to your JVisualVM executable. + +### MCEdit + +<!-- TODO: add image of MCEdit section --> + +This is where you set to path to your MCEdit executable. + +### External Editors + +<!-- TODO: add image of External Editors section --> + +This is where you set to path to your text editor of choice or leave it blank to use the systems default one. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/vanilla-platform.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/vanilla-platform.md new file mode 100644 index 0000000000..099b950fd8 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/vanilla-platform.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Custom / Vanilla + +![Custom tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance.png) + +This is where you can choose which minecraft version you want to play with and what mod loader to install. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/worlds.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/worlds.md new file mode 100644 index 0000000000..3ff1013bc7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/worlds.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Worlds + +![Worlds tab under ProjT Launcher Instances](/projtlauncher/img/screenshots/projtlauncher_dark_console_window_worlds.png) + +Here you can do some basic management of your worlds that you have in that Instance. diff --git a/archived/projt-launcher/docs/handbook/wiki/help-pages/zip-import.md b/archived/projt-launcher/docs/handbook/wiki/help-pages/zip-import.md new file mode 100644 index 0000000000..9570b428ef --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/help-pages/zip-import.md @@ -0,0 +1,7 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Zip Import + +![Import from zip tab under ProjT Launcher New Instances](/projtlauncher/img/screenshots/projtlauncher_dark_new_instance_import.png) + +Here you can import modpacks from a zip or mrpack file either from a website or from your filesystem. diff --git a/archived/projt-launcher/docs/handbook/wiki/overview/code-of-conduct.md b/archived/projt-launcher/docs/handbook/wiki/overview/code-of-conduct.md new file mode 100644 index 0000000000..8dd0f6379b --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/overview/code-of-conduct.md @@ -0,0 +1,91 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Contributor Covenant 3.0 Code of Conduct + +## Our Pledge + +We pledge to make our community welcoming, safe, and equitable for all. + +We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. + +## Encouraged Behaviors + +While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. + +With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: + +1. Respecting the **purpose of our community**, our activities, and our ways of gathering. +2. Engaging **kindly and honestly** with others. +3. Respecting **different viewpoints** and experiences. +4. **Taking responsibility** for our actions and contributions. +5. Gracefully giving and accepting **constructive feedback**. +6. Committing to **repairing harm** when it occurs. +7. Behaving in other ways that promote and sustain the **well-being of our community**. + +## Restricted Behaviors + +We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. + +1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. +2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. +3. **Stereotyping or discrimination.** Characterizing anyone?ì…² personality or behavior on the basis of immutable identities or traits. +4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. +5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. +6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. +7. Behaving in other ways that **threaten the well-being** of our community. + +### Other Restrictions + +1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. +2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. +3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. +4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. + +## Reporting an Issue + +Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. + +To report a possible violation, please contact the Community Moderators via +email at [yongdohyun@projtlauncher.yongdohyun.org.tr](mailto:yongdohyun@projtlauncher.yongdohyun.org.tr). + +All reports will be reviewed promptly and handled confidentially. + +Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. For this project, Community Moderators are the project maintainers. + +## Addressing and Repairing Harm + +The remedies and repairs outlined below are based on best practices in code of +conduct enforcement and are applied at the discretion of the project maintainers. + +If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. + +1) Warning + 1) Event: A violation involving a single incident or series of incidents. + 2) Consequence: A private, written warning from the Community Moderators. + 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. +2) Temporarily Limited Activities + 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. + 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. + 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. +3) Temporary Suspension + 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. + 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. + 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. +4) Permanent Ban + 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. + 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. + 3) Repair: There is no possible repair in cases of this severity. + +This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). + +Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) + +For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla?ì…² code of conduct team](https://github.com/mozilla/inclusion). diff --git a/archived/projt-launcher/docs/handbook/wiki/overview/copying.md b/archived/projt-launcher/docs/handbook/wiki/overview/copying.md new file mode 100644 index 0000000000..1d096974e7 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/overview/copying.md @@ -0,0 +1,821 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +## ProjT Launcher + + ProjT Launcher - Minecraft Launcher + Copyright (C) 2026 Project Tick + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + This project includes a modified version of the Prism Launcher logo. + + Original logo: + Prism Launcher Logo + © Prism Launcher Contributors + Licensed under CC BY-SA 4.0 + + Modified version: + ProjT Launcher Logo + © 2026 Project Tick + Licensed under CC BY-SA 4.0 + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## Prism Launcher + + Prism Launcher - Minecraft Launcher + Copyright (C) 2022-2025 Prism Launcher Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## PolyMC + + PolyMC - Minecraft Launcher + Copyright (C) 2021-2022 PolyMC Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## MultiMC + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## bzip2 (`bzip2/`) + + This project includes a forked copy of bzip2. + + Copyright (C) 1996–2010 Julian R Seward. + + Licensed under a BSD-style license. + + See: + - bzip2/COPYING + +## cmark (`cmark/`) + + This project includes a forked copy of cmark. + + Copyright (c) 2014 John MacFarlane. + + cmark contains code under multiple permissive licenses, including: + - BSD-2-Clause + - MIT + + Some files are derived from: + - houdini + - utf8proc + - GitHub, Inc. code + + Applicable license information is provided via SPDX headers in the source + files. Full license texts are available in LICENSES/. + +## extra-cmake-modules (`extra-cmake-modules/`) + + extra-cmake-modules is a collection of CMake modules originating from KDE. + + This component includes files under multiple permissive licenses, + including but not limited to: + + - MIT + - BSD-2-Clause + - BSD-3-Clause + - CC0-1.0 + + The applicable license for each file is documented via SPDX headers + and the corresponding license texts are available in: + + - extra-cmake-modules/LICENSES/ + - top-level LICENSES/ + + These licenses apply only to the extra-cmake-modules component and do + not affect the overall GPL-3.0-only licensing of ProjT Launcher. + +## launcherjava (`launcherjava/`) + + ProjT Launcher - Minecraft Launcher + Copyright (C) 2026 Project Tick + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License cover the whole + combination. + + As a special exception, the copyright holders of this library give + you permission to link this library with independent modules to + produce an executable, regardless of the license terms of these + independent modules, and to copy and distribute the resulting + executable under terms of your choice, provided that you also meet, + for each linked independent module, the terms and conditions of the + license of that module. An independent module is a module which is + not derived from or based on this library. If you modify this + library, you may extend this exception to your version of the + library, but you are not obliged to do so. If you do not wish to do + so, delete this exception statement from your version. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + Prism Launcher - Minecraft Launcher + Copyright (C) 2022-2025 Prism Launcher Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License cover the whole + combination. + + As a special exception, the copyright holders of this library give + you permission to link this library with independent modules to + produce an executable, regardless of the license terms of these + independent modules, and to copy and distribute the resulting + executable under terms of your choice, provided that you also meet, + for each linked independent module, the terms and conditions of the + license of that module. An independent module is a module which is + not derived from or based on this library. If you modify this + library, you may extend this exception to your version of the + library, but you are not obliged to do so. If you do not wish to do + so, delete this exception statement from your version. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + PolyMC - Minecraft Launcher + Copyright (C) 2021-2022 PolyMC Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + Linking this library statically or dynamically with other modules is + making a combined work based on this library. Thus, the terms and + conditions of the GNU General Public License cover the whole + combination. + + As a special exception, the copyright holders of this library give + you permission to link this library with independent modules to + produce an executable, regardless of the license terms of these + independent modules, and to copy and distribute the resulting + executable under terms of your choice, provided that you also meet, + for each linked independent module, the terms and conditions of the + license of that module. An independent module is a module which is + not derived from or based on this library. If you modify this + library, you may extend this exception to your version of the + library, but you are not obliged to do so. If you do not wish to do + so, delete this exception statement from your version. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + Copyright 2013-2021 MultiMC Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## JavaCheck (`javacheck/`) + + Project Tick JavaCheck - A simple Java system property checker + Copyright (C) 2026 Project Tick + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; version 2 + of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see + <https://www.gnu.org/licenses/>. + +## libnbtplusplus (`libnbtplusplus/`) + + libnbt++ 3 - A library for the Minecraft Named Binary Tag format v3. + Copyright (C) 2026 Project Tick + + libnbt++ 3 is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libnbt++ is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libnbt++ 3. If not, see <http://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + libnbt++ - A library for the Minecraft Named Binary Tag format. + Copyright (C) 2013, 2015 ljfa-ag + + libnbt++ is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libnbt++ is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libnbt++. If not, see <http://www.gnu.org/licenses/>. + +## libqrencode (`libqrencode/`) + + Project Tick libqrencode - Qr code encoder library + Copyright (C) 2026 Project Tick + + This library is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the Free + Software Foundation; either version 2.1 of the License, or any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along + with this library; if not, write to the Free Software Foundation, Inc., 51 + Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + This file incorporates work covered by the following copyright and + permission notice: + + Copyright (C) 2006-2018 Kentaro Fukuchi + + This library is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the Free + Software Foundation; either version 2.1 of the License, or any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along + with this library; if not, write to the Free Software Foundation, Inc., 51 + Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +## meta (`meta/`) + + Microsoft Public License (Ms-PL) + + This license governs use of the accompanying software. If you use the + software, you accept this license. If you do not accept the license, do not + use the software. + + 1. Definitions + The terms "reproduce," "reproduction," "derivative works," and "distribution" + have the same meaning here as under U.S. copyright law. A "contribution" is + the original software, or any additions or changes to the software. A + "contributor" is any person that distributes its contribution under this + license. "Licensed patents" are a contributor's patent claims that read + directly on its contribution. + + 2. Grant of Rights + (A) Copyright Grant- Subject to the terms of this license, including the + license conditions and limitations in section 3, each contributor grants + you a non-exclusive, worldwide, royalty-free copyright license to + reproduce its contribution, prepare derivative works of its contribution, + and distribute its contribution or any derivative works that you create. + + (B) Patent Grant- Subject to the terms of this license, including the + license conditions and limitations in section 3, each contributor grants + you a non-exclusive, worldwide, royalty-free license under its licensed + patents to make, have made, use, sell, offer for sale, import, and/or + otherwise dispose of its contribution in the software or derivative works + of the contribution in the software. + + 3. Conditions and Limitations + (A) No Trademark License- This license does not grant you rights to use + any contributors' name, logo, or trademarks. + + (B) If you bring a patent claim against any contributor over patents that + you claim are infringed by the software, your patent license from such + contributor to the software ends automatically. + + (C) If you distribute any portion of the software, you must retain all + copyright, patent, trademark, and attribution notices that are present in + the software. + + (D) If you distribute any portion of the software in source code form, + you may do so only under this license by including a complete copy of + this license with your distribution. If you distribute any portion of the + software in compiled or object code form, you may only do so under a + license that complies with this license. + + (E) The software is licensed "as-is." You bear the risk of using it. The + contributors give no express warranties, guarantees, or conditions. You + may have additional consumer rights under your local laws which this + license cannot change. To the extent permitted under your local laws, the + contributors exclude the implied warranties of merchantability, fitness + for a particular purpose and non-infringement. + +## program_info (`program_info/`) + +Logos and branding assets in this directory are licensed under +Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0). + +This license applies only to branding assets and does not affect +the licensing of the ProjT Launcher source code. + +See: +- program_info/LICENSE + +## quazip (`quazip/`) + + Project Tick quazip - ZIP archive library for Qt + Copyright (C) 2010 Roberto Pompermaier + Copyright (C) 2005-2014 Sergey A. Tachenov + + QuaZip is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 2.1 of the License, or + (at your option) any later version. + + QuaZip is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with QuaZip. If not, see <http://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + Copyright (C) 2010 Roberto Pompermaier + Copyright (C) 2005-2014 Sergey A. Tachenov + + QuaZip is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 2.1 of the License, or + (at your option) any later version. + + QuaZip is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with QuaZip. If not, see <http://www.gnu.org/licenses/>. + +## tomlplusplus (`tomlplusplus/`, `website/tomlplusplus/`) + + MIT License + + Copyright (c) Mark Gillard <mark.gillard@outlook.com.au> + Copyright (c) 2026 Project Tick + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is furnished + to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS + OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF + OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## website ProjT Launcher (`website/projtlauncher/`) + + ProjT Launcher - Minecraft Launcher + Copyright (C) 2026 Project Tick + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + + This file incorporates work covered by the following copyright and + permission notice: + + Prism Launcher - Minecraft Launcher + Copyright (C) 2022-2025 Prism Launcher Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +## Project Tick Website and Handbook + + Project Tick Website and Handbook - Website and Handbook + Copyright (C) 2026 Project Tick + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +## zlib + + Copyright notice: + + (C) 1995-2025 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +## MinGW-w64 runtime (Windows) + + Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project + + This license has been certified as open source. It has also been designated + as GPL compatible by the Free Software Foundation (FSF). + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions in source code must retain the accompanying copyright + notice, this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the accompanying + copyright notice, this list of conditions, and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + 3. Names of the copyright holders must not be used to endorse or promote + products derived from this software without prior written permission + from the copyright holders. + 4. The right to distribute this software or to use it for any purpose does + not give you the right to use Servicemarks (sm) or Trademarks (tm) of + the copyright holders. Use of them is covered by separate agreement + with the copyright holders. + 5. If any files are modified, you must cause the modified files to carry + prominent notices stating that you changed the files and the date of + any change. + + Disclaimer + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. + +## Qt 6 + + Copyright (C) 2022 The Qt Company Ltd and other contributors. + Contact: https://www.qt.io/licensing + + Licensed under LGPL v3 + +## rainbow (KGuiAddons) + + Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net> + Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org> + Copyright (C) 2007 Thomas Zander <zander@kde.org> + Copyright (C) 2007 Zack Rusin <zack@kde.org> + Copyright (C) 2015 Petr Mrazek <peterix@gmail.com> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + +## Batch icon set + + You are free to use Batch (the "icon set") or any part thereof (the "icons") + in any personal, open-source or commercial work without obligation of payment + (monetary or otherwise) or attribution. Do not sell the icon set, host + the icon set or rent the icon set (either in existing or modified form). + + While attribution is optional, it is always appreciated. + + Intellectual property rights are not transferred with the download of the icons. + + EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL ADAM WHITCROFT + BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, + PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS, + EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## Material Design Icons + + Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/), + with Reserved Font Name Material Design Icons. + Copyright (c) 2014, Google (http://www.google.com/design/) + uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE + + This Font Software is licensed under the SIL Open Font License, Version 1.1. + This license is copied below, and is also available with a FAQ at: + http://scripts.sil.org/OFL + +## lionshead + + Code has been taken from https://github.com/natefoo/lionshead and loosely + translated to C++ laced with Qt. + + MIT License + + Copyright (c) 2017 Nate Coraor + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +## Gamemode + + Copyright (c) 2017-2022, Feral Interactive + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Feral Interactive nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +## Breeze icons + + Copyright (C) 2014 Uri Herrera <uri_herrera@nitrux.in> and others + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see <http://www.gnu.org/licenses/>. + +## Oxygen Icons + + The Oxygen Icon Theme + Copyright (C) 2007 Nuno Pinheiro <nuno@oxygen-icons.org> + Copyright (C) 2007 David Vignoni <david@icon-king.com> + Copyright (C) 2007 David Miller <miller@oxygen-icons.org> + Copyright (C) 2007 Johann Ollivier Lapeyre <johann@oxygen-icons.org> + Copyright (C) 2007 Kenneth Wimer <kwwii@bootsplash.org> + Copyright (C) 2007 Riccardo Iaconelli <riccardo@oxygen-icons.org> + + and others + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see <http://www.gnu.org/licenses/>. + +## vcpkg (`cmake/vcpkg-ports`) + + MIT License + + Copyright (c) Microsoft Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this + software and associated documentation files (the "Software"), to deal in the Software + without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be included in all copies + or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/archived/projt-launcher/docs/handbook/wiki/overview/faq.md b/archived/projt-launcher/docs/handbook/wiki/overview/faq.md new file mode 100644 index 0000000000..545a8c750c --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/overview/faq.md @@ -0,0 +1,9 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Frequently Asked Questions + +## What is ProjT Launcher? + +ProjT was forked from the PrismLauncher project in late 2025. + +The original fork was a result of continuing disagreements between a portion of the community and MultiMC's lead maintainer, which ultimately led to the creation of Prism Launcher. However, ProjT Launcher itself forked from the Prism Launcher codebase. ProjT's main motivation is not only to uphold the freedom of 3rd party packaging and re-distribution but also to offer an alternative for users dissatisfied with Prism's development pace, aiming to be a project that focuses on adding new features and is generally more open to development. diff --git a/archived/projt-launcher/docs/handbook/wiki/overview/feedback-bugs.md b/archived/projt-launcher/docs/handbook/wiki/overview/feedback-bugs.md new file mode 100644 index 0000000000..3f22810edb --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/overview/feedback-bugs.md @@ -0,0 +1,9 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Feedback and Bugs + +We really appreciate all kinds of feedback from our community. If you've run into a bug while using ProjT Launcher, or if you'd like to suggest our next killer feature, please head over to our [GitHub Issues](https://github.com/Project-Tick/ProjT-Launcher/issues) page and make a post. + +**NOTE:** If you're reporting a bug, please do have a good look through the list of issues posted by other users. It may have already been reported by someone else, or better yet, fixed! + +Once you're done there, why don't you come and [join us](https://projecttick.org/projtlauncher/#get-involved) on our Discord server, Matrix Space, or subreddit? ~~*Or how about all three?*~~ diff --git a/archived/projt-launcher/docs/handbook/wiki/overview/frequent-issues.md b/archived/projt-launcher/docs/handbook/wiki/overview/frequent-issues.md new file mode 100644 index 0000000000..9147c178ab --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/overview/frequent-issues.md @@ -0,0 +1,78 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Troubleshooting + +This is a collection of helpful information for frequent problems when using ProjT Launcher. + +## Minecraft 1.16.5 with Minecraft Forge and Java 8u321+ + +Older versions of Minecraft Forge for Minecraft 1.16.5 cause the game to crash on launch, as they do not support the latest revisions of Java 8. + +You can fix this by changing the Forge version in the launcher to the latest or recommended version. +For that go to **Version** ??**Select Forge** ??**Change Version** ??**Select newest version** ??**OK** + +Alternatively you can download and use an older Java version (i.e. Java 8u312) + +## Minecraft Forge failing to run processor + +If you are getting an error saying `Failed to run processor: java.net.ConnectException: Connection refused` when trying to launch a Minecraft Forge instance, you might have connectivity issues with IPv6. + +A workaround for this issue is adding the following JVM argument: + +```text +-Djava.net.preferIPv4Stack=true +``` + +## Common Launcher-related issues + +### <img src="https://upload.wikimedia.org/wikipedia/commons/8/87/Windows_logo_-_2021.svg" alt="Windows Logo" height="20" /> Windows (7, 8.1, 10, 11) + +#### "MSVCP140_2.dll was not found"? + +Since ProjT Launcher 0.0.1, ProjT is compiled using [MSVC](https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B) on Windows. +As a consequence of this, like most apps on Windows, you have to install vcredist for ProjT to run. +You need: + +* [vcredist 2022 x64](https://aka.ms/vs/17/release/vc_redist.x64.exe) if you're using ProjTLauncher-Windows-MSVC (the recommended version for Windows 10 64 bit/Windows 11) +* [vcredist 2022 arm64](https://aka.ms/vs/17/release/vc_redist.arm64.exe) if you're using ProjTLauncher-Windows-MSVC-arm64 (the recommended version for Windows 10/11 on ARM) + +#### How do I open a .zip file? + +Windows by default can "open" **.zip** archive files, but in order to use ProjT Launcher, you will want to **extract** it instead. + +#### Windows Protected my PC? + +This is unfortunately **normal behaviour** due to the nature of the Windows app signing process. ProjT Launcher has yet to purchase a signature, however, with enough funding, we may choose to do so in the future. ProjT Launcher is an **open-source** application. As a result of this, all of the source code is public, and can be audited by any individual or group. If you would like to do so yourself, you can do so here: <https://github.com/Project-Tick/ProjT-Launcher> + +If you are **comfortable** and **trust** ProjT Launcher, then you can click on the **More info** button, and then do the same on the **Run anyway** one too. + +### <img src="https://upload.wikimedia.org/wikipedia/commons/3/3c/TuxFlat.svg" alt="Linux Tux Logo" height="20" /> Linux + +<!-- #### How do I install the ProjT Launcher Flatpak on my Linux system? + +Detailed instructions on setting-up your system to install Flatpak applications from Flathub, can be found here: <https://flatpak.org/setup/> --> + +#### How do I open the ProjT Launcher AppImage on my Linux system? + +Depending on your system, you may need to grant the ProjT Launcher AppImage **executable** permissions. + +You can do this by opening your system's terminal application, **making sure to navigate to the location of the downloaded AppImage,** before granting the execute permission using this command: + +```bash +sudo chmod +x ProjTLauncher-Linux-{{version.current}}-x86_64.AppImage +``` + +**Please note,** that depending on the version of ProjT Launcher that you have downloaded, you may have to **change the version number** in the command above. + +If you want to simplify the installation of the AppImage, use [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher). Note that this won't work on non-systemd system, and we recommend just using packages. + +#### ProjT Launcher isn't using my system theme + +ProjT Launcher has support for Qt 6. +This means that some themes and theming platforms like KDE Plasma's theming will not work on builds using that version of the Qt toolkit. + +### <img src="https://upload.wikimedia.org/wikipedia/commons/8/87/Windows_logo_-_2021.svg" alt="Windows Logo" height="20" /> <img src="https://upload.wikimedia.org/wikipedia/commons/3/3c/TuxFlat.svg" alt="Linux Tux Logo" height="20" /> Windows and Linux + +#### I want to make my system install portable + +On ProjT Launcher you can make any install portable (or making portable installs system) just by adding (or removing) portable.txt to the ProjT Launcher root directory. diff --git a/archived/projt-launcher/docs/handbook/wiki/overview/index.md b/archived/projt-launcher/docs/handbook/wiki/overview/index.md new file mode 100644 index 0000000000..297c69f165 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/overview/index.md @@ -0,0 +1,11 @@ +> Wiki navigation: [Overview](/handbook/wiki/) | [Getting Started](/handbook/wiki/getting-started/) | [Help Pages](/handbook/wiki/help-pages/) | [Development](/handbook/wiki/development/) + +# Welcome to the ProjT Launcher Wiki! + +If you're new to ProjT Launcher and not quite sure where to begin, please go to [Getting Started](./getting-started). + +ProjT Launcher is a free and open source Minecraft launcher forked from the Prism Launcher project, with the ability to manage multiple accounts, as well as instances, each with their own mods, resource packs, and more! Our project has a much greater focus on both user-freedom, and the incorporation of new and exciting features and functionality. + +Please **do not** contact the Prism Launcher team regarding issues related ProjT Launcher. + +![Launcher](/projtlauncher/img/screenshots/launcher.png) diff --git a/archived/projt-launcher/docs/handbook/wiki/wiki.json b/archived/projt-launcher/docs/handbook/wiki/wiki.json new file mode 100644 index 0000000000..43afe8e184 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/wiki/wiki.json @@ -0,0 +1,3 @@ +{ + "layout": "handbook.njk" +} diff --git a/archived/projt-launcher/docs/handbook/workflows.md b/archived/projt-launcher/docs/handbook/workflows.md new file mode 100644 index 0000000000..8b9a658cc9 --- /dev/null +++ b/archived/projt-launcher/docs/handbook/workflows.md @@ -0,0 +1,226 @@ +# GitHub Actions Workflows + +> **Location**: `.github/workflows/` +> **Platform**: GitHub Actions +> **Type**: CI/CD Pipeline +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +ProjT Launcher uses a modular GitHub Actions workflow architecture. The CI system is split into specialized sub-workflows that are orchestrated by a main coordinator workflow. + +--- + +## Workflow Architecture + +``` +┌─────────────────────────────────────────────────────────────┠+│ ci-new.yml │ +│ (Main Orchestrator) │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┠┌─────────────┠┌─────────────────────┠│ +│ │ ci-prepare │ │ ci-lint │ │ ci-release │ │ +│ │ .yml │ │ .yml │ │ .yml │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┠│ +│ │ Library Workflows │ │ +│ │ ┌─────────┠┌─────────┠┌─────────┠┌───────────┠│ │ +│ │ │ci-zlib │ │ci-bzip2 │ │ci-quazip│ │ ci-cmark │ │ │ +│ │ │ .yml │ │ .yml │ │ .yml │ │ .yml │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └───────────┘ │ │ +│ │ ┌──────────────┠┌────────────────┠│ │ +│ │ │ci-tomlplusplus│ │ ci-libqrencode │ │ │ +│ │ │ .yml │ │ .yml │ │ │ +│ │ └──────────────┘ └────────────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────┠┌─────────────┠┌─────────────────────┠│ +│ │ ci-website │ │ci-scheduled │ │ (unlock) │ │ +│ │ .yml │ │ .yml │ │ Merge Gate │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Workflow Files + +### Main Orchestrator + +| File | Description | +|------|-------------| +| `ci-new.yml` | Main coordinator that calls all sub-workflows with conditional logic | + +### Core Workflows + +| File | Purpose | Triggers | +|------|---------|----------| +| `ci-prepare.yml` | Initial setup, dependency caching | Called by main | +| `ci-lint.yml` | Code quality checks (clang-format, linters) | Push, PR, Manual | +| `ci-release.yml` | Build releases, create artifacts | Release, Manual | +| `ci-website.yml` | Build and deploy website | Push, PR | +| `ci-scheduled.yml` | Scheduled maintenance tasks | Cron | + +### Library Workflows + +Each bundled library has its own independent CI: + +| File | Library | Tests | +|------|---------|-------| +| `ci-zlib.yml` | zlib | Compression tests | +| `ci-bzip2.yml` | bzip2 | Compression + valgrind | +| `ci-quazip.yml` | QuaZip | Qt ZIP operations | +| `ci-cmark.yml` | cmark | Markdown parsing | +| `ci-tomlplusplus.yml` | toml++ | TOML parsing | +| `ci-libqrencode.yml` | libqrencode | QR generation | + +--- + +## Workflow Design Principles + +### 1. Modular Architecture + +Each workflow is self-contained and can be run independently: + +```yaml +# Sub-workflow pattern +on: + workflow_call: + inputs: + ref: + required: false + type: string + push: + paths: + - 'library-name/**' + pull_request: + paths: + - 'library-name/**' +``` + +### 2. Centralized Control + +The main orchestrator (`ci-new.yml`) controls when each sub-workflow runs: + +```yaml +jobs: + call-zlib: + if: needs.prepare.outputs.run-zlib == 'true' + uses: ./.github/workflows/ci-zlib.yml +``` + +### 3. Path Filtering + +Workflows only run when relevant files change: + +```yaml +paths: + - 'zlib/**' + - '.github/workflows/ci-zlib.yml' +``` + +### 4. Merge Gate + +The `unlock` job aggregates all results for merge protection: + +{% raw %} +```yaml +unlock: + needs: [lint, build, test-zlib, test-bzip2, ...] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check all jobs + run: | + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + exit 1 + fi +``` +{% endraw %} + +--- + +## Triggers + +| Trigger | Workflows | Description | +|---------|-----------|-------------| +| `push` | All | Commits to main branches | +| `pull_request` | All | Pull request changes | +| `release` | Release | GitHub releases | +| `schedule` | Scheduled | Cron jobs | +| `workflow_dispatch` | All | Manual triggers | +| `workflow_call` | Sub-workflows | Called by orchestrator | + +--- + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `GITHUB_TOKEN` | Automatic GitHub token | +| `MSA_CLIENT_ID` | Microsoft Auth client ID | +| `CACHIX_AUTH_TOKEN` | Nix cache auth | + +--- + +## Adding New Workflows + +1. **Create the workflow file** in `.github/workflows/` +2. **Add workflow_call trigger** for orchestrator integration +3. **Add push/PR triggers** for independent execution +4. **Update ci-new.yml** to include the new workflow +5. **Document** in this handbook + +### Template + +{% raw %} +```yaml +name: CI Library Name + +on: + workflow_call: + inputs: + ref: + required: false + type: string + push: + paths: + - 'library-name/**' + - '.github/workflows/ci-libraryname.yml' + pull_request: + paths: + - 'library-name/**' + - '.github/workflows/ci-libraryname.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + # Build steps... +``` +{% endraw %} + +--- + +## Related Documentation + +- [CI Support Files](./ci_support.md) — Configuration files +- [CI Evaluation](./ptcieval.md) — Nix-based validation +- [GitHub Scripts](./ptcigh.md) — Automation helpers +- [Bot](./bot.md) — PR automation + +--- + +## External Links + +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Workflow Syntax](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions) +- [Reusable Workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) diff --git a/archived/projt-launcher/docs/handbook/zlib.md b/archived/projt-launcher/docs/handbook/zlib.md new file mode 100644 index 0000000000..f98183e21d --- /dev/null +++ b/archived/projt-launcher/docs/handbook/zlib.md @@ -0,0 +1,134 @@ +# Zlib `zlib/` + +> **Type**: Compression Library +> **License**: zlib License +> **Fork Origin**: [zlib.net](https://zlib.net) | [GitHub](https://github.com/madler/zlib) +> **Status**: Detached Fork (independently maintained) +> **Base Version**: 1.3.1.2 +> **Latest Version**: 0.0.5-1 + +--- + +## Overview + +Zlib is a general-purpose, lossless data-compression library used for ZIP and GZip functionality. It implements the DEFLATE compression algorithm and is one of the most widely used compression libraries in the world. + +This repository contains a maintained fork of the upstream zlib project. The fork exists to allow controlled integration, CI validation, and long-term maintenance within the ProjT Launcher monorepo. + +--- + +## Fork Policy + +This is a **detached fork** that is independently maintained by Project Tick: + +- ✅ Original codebase used as foundation +- ✅ Independent development and maintenance +- ✅ Custom CI/CD integration for ProjT Launcher +- âš ï¸ May diverge from original project over time + +This fork is not synchronized with the original repository. + +--- + +## Usage in ProjT Launcher + +Zlib is used for: + +- **ZIP archive handling** via QuaZip wrapper +- **PNG image compression** for textures and assets +- **Network compression** for mod downloads +- **NBT file compression** for Minecraft world data + +--- + +## Documentation + +| Resource | Location | +|----------|----------| +| API Reference | `zlib/zlib.h` | +| FAQ | `zlib/FAQ` | +| Change History | `zlib/ChangeLog` | +| [Original CMake README](../../zlib/README-cmake.md) | Upstream CMake documentation | +| Upstream Manual | [zlib.net/manual.html](https://zlib.net/manual.html) | + +--- + +## Build Integration + +Zlib is built as part of the ProjT Launcher build system. The upstream build systems are preserved for reference, but the primary build path uses our integrated CMake configuration. + +### CMake Options + +```cmake +# In the main CMakeLists.txt, zlib is included as a subdirectory +add_subdirectory(zlib) + +# Link against zlib +target_link_libraries(your_target PRIVATE ZLIB::ZLIB) +``` + +### Standalone Build + +For development or testing purposes: + +```bash +cd zlib +mkdir build && cd build +cmake .. +cmake --build . +``` + +For upstream-specific build instructions, see [zlib.net](https://zlib.net). + +--- + +## Testing + +Zlib includes its own test suite that is run as part of CI: + +```bash +cd zlib/build +ctest -V +``` + +See [ci-zlib.yml](../../.github/workflows/ci-zlib.yml) for CI configuration. + +--- + +## Licensing + +Zlib is licensed under the **zlib License**, a permissive free software license. + +The full license text is included unmodified in `zlib/LICENSE`. + +### Copyright + +**Original Work:** +``` +Copyright © 1995–2025 +Jean-loup Gailly, Mark Adler +``` + +**Modifications:** +``` +Copyright © 2026 +Project Tick contributors +``` + +--- + +## Related Documentation + +- [QuaZip](./quazip.md) — C++ wrapper that uses zlib +- [bzip2](./bzip2.md) — Alternative compression library +- [Third-party Libraries](./third-party.md) — Overview of all dependencies + +--- + +## External Links + +- [zlib Official Website](https://zlib.net) +- [zlib Official GitHub Repository](https://github.com/madler/zlib) +- [zlib Project Tick Github Repository](https://github.com/Project-Tick/ProjT-Launcher/tree/main/zlib) +- [RFC 1950 - ZLIB Specification](https://tools.ietf.org/html/rfc1950) +- [RFC 1951 - DEFLATE Specification](https://tools.ietf.org/html/rfc1951) diff --git a/archived/projt-launcher/docs/packaging/README.md b/archived/projt-launcher/docs/packaging/README.md new file mode 100644 index 0000000000..dc9dbe4caf --- /dev/null +++ b/archived/projt-launcher/docs/packaging/README.md @@ -0,0 +1,8 @@ +# Packaging + +Platform-specific packaging files. + +For documentation, see: + +- [Linux Packaging](../handbook/linux-packaging.md) +- [Nix Packaging](../handbook/nix.md) diff --git a/archived/projt-launcher/docs/packaging/os-specific/linux/flathub/flathubpackage.yml b/archived/projt-launcher/docs/packaging/os-specific/linux/flathub/flathubpackage.yml new file mode 100644 index 0000000000..07ef6e05c7 --- /dev/null +++ b/archived/projt-launcher/docs/packaging/os-specific/linux/flathub/flathubpackage.yml @@ -0,0 +1,110 @@ +id: org.projecttick.ProjTLauncher +runtime: org.kde.Platform +runtime-version: '6.10' +sdk: org.kde.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.openjdk17 + +command: projtlauncher +finish-args: + - --share=ipc + - --socket=wayland + - --socket=fallback-x11 + - --device=all # Grant access to all device nodes, often required by certain game mods for accessing specialized hardware like controllers, joysticks, or other input peripherals that use non-standard device paths. + - --share=network + - --socket=pulseaudio + # for Discord RPC mods + - --filesystem=xdg-run/app/com.discordapp.Discord:create + # Mod drag&drop + - --filesystem=xdg-download:ro + # FTBApp import + - --persist=.ftba + # Userspace visibility for manual hugepages configuration + # Required for -XX:+UseLargePages + - --filesystem=/sys/kernel/mm/hugepages:ro + # Userspace visibility for transparent hugepages configuration + # Required for -XX:+UseTransparentHugePages + - --filesystem=/sys/kernel/mm/transparent_hugepage:ro + +cleanup: + - /include + - /lib/cmake + - /lib/pkgconfig + +modules: + - name: glfw + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=RelWithDebInfo + - -DBUILD_SHARED_LIBS:BOOL=ON + - -DGLFW_BUILD_WAYLAND:BOOL=ON + - -DGLFW_BUILD_DOCS:BOOL=OFF + sources: + - type: git + url: https://github.com/glfw/glfw.git + tag: '3.4' + commit: 7b6aead9fb88b3623e3b3725ebb42670cbe4c579 + + - name: xrandr + buildsystem: autotools + sources: + - type: archive + url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz + sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c + x-checker-data: + type: anitya + project-id: 14957 + stable-only: true + url-template: https://xorg.freedesktop.org/archive/individual/app/xrandr-$version.tar.xz + cleanup: + - /share/man + - /bin/xkeystone + + - name: gamemode + buildsystem: meson + config-opts: + - -Dwith-sd-bus-provider=no-daemon + - -Dwith-examples=false + post-install: + # gamemoderun is installed for users who want to use wrapper commands + # post-install is running inside the build dir, we need it from the source though + - install -Dm755 ../data/gamemoderun -t /app/bin + sources: + - type: archive + dest-filename: gamemode.tar.gz + url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.2 + sha256: 2886d4ce543c78bd2a364316d5e7fd59ef06b71de63f896b37c6d3dc97658f60 + x-checker-data: + type: json + url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest + version-query: .tag_name + url-query: .tarball_url + timestamp-query: .published_at + cleanup: + - /include + - /lib/pkgconfig + - '*.a' + + - name: projtlauncher + buildsystem: cmake-ninja + builddir: true + config-opts: + - -DLauncher_BUILD_PLATFORM=flatpak + # This allows us to manage and update Java independently of this Flatpak + - -DLauncher_ENABLE_JAVA_DOWNLOADER=ON + - -DCMAKE_BUILD_TYPE=RelWithDebInfo + build-options: + env: + JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 + JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac + run-tests: true + post-install: + - mv /app/bin/projtlauncher /app/bin/projtrun + - install -Dm755 ../projtlauncher -t /app/bin + sources: + - type: git + url: https://github.com/Project-Tick/ProjT-Launcher.git + tag: 0.0.3-4 + commit: f0d189dc9dcd5938cc52f385b610165f65198da8 # 0.0.3-4 + - type: file + path: projtlauncher diff --git a/archived/projt-launcher/docs/packaging/os-specific/linux/flathub/projtlauncher b/archived/projt-launcher/docs/packaging/os-specific/linux/flathub/projtlauncher new file mode 100644 index 0000000000..90ea33e4e4 --- /dev/null +++ b/archived/projt-launcher/docs/packaging/os-specific/linux/flathub/projtlauncher @@ -0,0 +1,11 @@ +#!/bin/bash + +# discord RPC +for i in {0..9}; do + test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i"; +done + +export PATH="${PATH}${PATH:+:}/usr/lib/extensions/vulkan/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin" +export VK_LAYER_PATH="/usr/lib/extensions/vulkan/share/vulkan/implicit_layer.d/" + +exec /app/bin/projtrun "$@" diff --git a/archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher-unwrapped.nix b/archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher-unwrapped.nix new file mode 100644 index 0000000000..505792b9d6 --- /dev/null +++ b/archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher-unwrapped.nix @@ -0,0 +1,109 @@ +{ + lib, + stdenv, + fetchFromGitHub, + cmake, + cmark, + extra-cmake-modules, + qt6Packages, + gamemode, + jdk17, + ninja, + nix-update-script, + stripJavaArchivesHook, + tomlplusplus, + zlib, + msaClientID ? null, + gamemodeSupport ? stdenv.hostPlatform.isLinux, +}: + +assert lib.assertMsg ( + gamemodeSupport -> stdenv.hostPlatform.isLinux +) "gamemodeSupport is only available on Linux."; +stdenv.mkDerivation (finalAttrs: { + pname = "projtlauncher"; + version = "0.0.3-4"; + + src = fetchFromGitHub { + owner = "Project-Tick"; + repo = "ProjT-Launcher"; + tag = finalAttrs.version; + hash = "sha256-U7bcQOySof86EtZHa63/PNuWDDJ7kEL0NuzvJ1dGU8c="; + fetchSubmodules = true; + }; + + nativeBuildInputs = [ + cmake + ninja + jdk17 + stripJavaArchivesHook + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + extra-cmake-modules + ]; + + buildInputs = [ + cmark + qt6Packages.qtbase + qt6Packages.qtnetworkauth + qt6Packages.quazip + tomlplusplus + zlib + ] + ++ lib.optional gamemodeSupport gamemode; + + cmakeFlags = [ + # downstream branding + (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") + ] + ++ lib.optionals (msaClientID != null) [ + (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + # we wrap our binary manually + (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") + # disable built-in updater + (lib.cmakeFeature "MACOSX_SPARKLE_UPDATE_FEED_URL" "''") + (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") + ]; + + doCheck = true; + + dontWrapQtApps = true; + + passthru = { + updateScript = nix-update-script { }; + }; + + meta = { + description = "Free, open-source Minecraft launcher"; + longDescription = '' + This application lets you create and manage multiple + independent Minecraft instances, each with its own + unique mods, texture packs, worlds, and settings. + Easily switch between different setups without conflicts, + keep all your saves and customizations organized, and + configure options for each instance through a simple and + intuitive interface. Perfect for players who want to + experiment with different modpacks, resource packs, or + gameplay styles while keeping everything neatly separated. + ''; + homepage = "https://projecttick.org/projtlauncher"; + changelog = "https://github.com/Project-Tick/ProjT-Launcher/releases/tag/${finalAttrs.version}"; + license = with lib.licenses; [ + gpl3Only + asl20 + cc-by-sa-40 + ]; + maintainers = with lib.maintainers; [ + yongdohyun + grxtor + ]; + mainProgram = "projtlauncher"; + platforms = [ + "x86_64-linux" + "aarch64-linux" + "aarch64-darwin" + ]; + }; +}) diff --git a/archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher.nix b/archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher.nix new file mode 100644 index 0000000000..d597a275f4 --- /dev/null +++ b/archived/projt-launcher/docs/packaging/os-specific/linux/nix/projtlauncher.nix @@ -0,0 +1,132 @@ +{ + addDriverRunpath, + alsa-lib, + flite, + gamemode, + glfw3-minecraft, + jdk17, + jdk21, + jdk8, + qt6Packages, + lib, + libGL, + libX11, + libXcursor, + libXext, + libXrandr, + libXxf86vm, + libjack2, + libpulseaudio, + libusb1, + mesa-demos, + openal, + pciutils, + pipewire, + projtlauncher-unwrapped, + stdenv, + symlinkJoin, + udev, + vulkan-loader, + xrandr, + + additionalLibs ? [ ], + additionalPrograms ? [ ], + controllerSupport ? stdenv.hostPlatform.isLinux, + gamemodeSupport ? stdenv.hostPlatform.isLinux, + jdks ? [ + jdk21 # need for newest Minecraft versions + jdk17 # need for old and new Minecraft versions + jdk8 # need for legacy Minecraft versions + ], + msaClientID ? null, + textToSpeechSupport ? stdenv.hostPlatform.isLinux, +}: + +assert lib.assertMsg ( + controllerSupport -> stdenv.hostPlatform.isLinux +) "controllerSupport only has an effect on Linux."; + +assert lib.assertMsg ( + textToSpeechSupport -> stdenv.hostPlatform.isLinux +) "textToSpeechSupport only has an effect on Linux."; + +let + projtlauncher' = projtlauncher-unwrapped.override { inherit msaClientID gamemodeSupport; }; +in + +symlinkJoin { + pname = "projtlauncher"; + inherit (projtlauncher') version; + + paths = [ projtlauncher' ]; + + nativeBuildInputs = [ qt6Packages.wrapQtAppsHook ]; + + buildInputs = [ + qt6Packages.qtbase + qt6Packages.qtsvg + ] + ++ lib.optional stdenv.hostPlatform.isLinux qt6Packages.qtwayland; + + postBuild = '' + wrapQtAppsHook + ''; + + qtWrapperArgs = + let + runtimeLibs = [ + (lib.getLib stdenv.cc.cc) + ## native versions + glfw3-minecraft + openal + + ## openal + alsa-lib + libjack2 + libpulseaudio + pipewire + + ## glfw + libGL + libX11 + libXcursor + libXext + libXrandr + libXxf86vm + + udev # oshi + + vulkan-loader # VulkanMod's lwjgl + ] + ++ lib.optional textToSpeechSupport flite + ++ lib.optional gamemodeSupport gamemode.lib + ++ lib.optional controllerSupport libusb1 + ++ additionalLibs; + + runtimePrograms = [ + mesa-demos + pciutils # need lspci + xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 + ] + ++ additionalPrograms; + + in + [ "--prefix PROJTLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}" ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + "--set LD_LIBRARY_PATH ${addDriverRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" + "--prefix PATH : ${lib.makeBinPath runtimePrograms}" + ]; + + meta = { + inherit (projtlauncher'.meta) + description + longDescription + homepage + changelog + license + maintainers + mainProgram + platforms + ; + }; +} diff --git a/archived/projt-launcher/docs/packaging/os-specific/macos/homebrew/projtlauncher.rb b/archived/projt-launcher/docs/packaging/os-specific/macos/homebrew/projtlauncher.rb new file mode 100644 index 0000000000..f8185dfc67 --- /dev/null +++ b/archived/projt-launcher/docs/packaging/os-specific/macos/homebrew/projtlauncher.rb @@ -0,0 +1,31 @@ +cask "projtlauncher" do + version "0.0.3-4" + depends_on macos: :big_sur + + sha256 "68ff5bdaf1e92675a4a660e5b6bf42a989aae30dd6680856e7f691ea44809aec" + + url "https://github.com/Project-Tick/ProjT-Launcher/releases/download/#{version}/ProjTLauncher-macOS-#{version}.zip", + verified: "github.com/Project-Tick/ProjT-Launcher/" + + name "ProjT Launcher" + desc "Minecraft launcher" + homepage "https://projecttick.org/projtlauncher/" + + livecheck do + url "https://projecttick.org/projtlauncher/feed/appcast.xml" + strategy :sparkle + end + + auto_updates true + + app "ProjT Launcher.app" + binary "#{appdir}/ProjT Launcher.app/Contents/MacOS/projtlauncher" + + zap trash: [ + "~/Library/Application Support/ProjT Launcher/metacache", + "~/Library/Application Support/ProjT Launcher/projtlauncher-*.log", + "~/Library/Application Support/ProjT Launcher/projtlauncher.cfg", + "~/Library/Preferences/org.projecttick.ProjTLauncher.plist", + "~/Library/Saved Application State/org.projecttick.ProjTLauncher.savedState", + ] +end \ No newline at end of file diff --git a/archived/projt-launcher/flake.lock b/archived/projt-launcher/flake.lock new file mode 100644 index 0000000000..6f6bfa6fc8 --- /dev/null +++ b/archived/projt-launcher/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1769461804, + "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/archived/projt-launcher/flake.nix b/archived/projt-launcher/flake.nix new file mode 100644 index 0000000000..1ad4eb7a95 --- /dev/null +++ b/archived/projt-launcher/flake.nix @@ -0,0 +1,217 @@ +{ + description = "ProjT Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of PrismLauncher)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { + self, + nixpkgs, + }: + + let + inherit (nixpkgs) lib; + + # While we only officially support aarch and x86_64 on Linux and MacOS, + # we expose a reasonable amount of other systems for users who want to + # build for most exotic platforms + systems = lib.systems.flakeExposed; + + forAllSystems = lib.genAttrs systems; + nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); + in + + { + checks = forAllSystems ( + system: + + let + pkgs = nixpkgsFor.${system}; + llvm = pkgs.llvmPackages_19; + in + + { + formatting = + pkgs.runCommand "check-formatting" + { + nativeBuildInputs = with pkgs; [ + deadnix + llvm.clang-tools + markdownlint-cli + nixfmt-rfc-style + statix + ]; + } + '' + cd ${self} + + echo "Running clang-format...." + clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} + + echo "Running deadnix..." + deadnix --fail + + echo "Running markdownlint..." + markdownlint --dot . + + echo "Running nixfmt..." + find -type f -name '*.nix' -exec nixfmt --check {} + + + echo "Running statix" + statix check . + + touch $out + ''; + } + ); + + devShells = forAllSystems ( + system: + + let + pkgs = nixpkgsFor.${system}; + llvm = pkgs.llvmPackages_19; + + packages' = self.packages.${system}; + + welcomeMessage = '' + + Welcome to the ProjT launcher project! ✨ + + We just set some things up for you. To get building, you can run: + + ``` + $ cd "$cmakeBuildDir" + $ ninjaBuildPhase + $ ninjaInstallPhase + ``` + + And thanks for helping out :) + ''; + + # Re-use our package wrapper to wrap our development environment. + # We override the unwrapped package with a dummy to avoid triggering + # a build of the entire project just to enter the dev shell. + qt-wrapper-env = (packages'.projtlauncher.override { + projtlauncher-unwrapped = packages'.projtlauncher-unwrapped.overrideAttrs (old: { + src = pkgs.runCommand "empty-src" { } "mkdir $out"; + dontUnpack = true; + configurePhase = "true"; + buildPhase = "true"; + installPhase = "mkdir -p $out/lib/projtlauncher"; + doCheck = false; + }); + }).overrideAttrs (old: { + name = "qt-wrapper-env"; + + # Required to use script-based makeWrapper below + strictDeps = true; + + # We don't need/want the unwrapped ProjT package + paths = [ ]; + + nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [ + # Ensure the wrapper is script based so it can be sourced + pkgs.makeWrapper + ]; + + # Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10 + buildCommand = '' + + makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out" + sed -i '/^exec/d' "$out" + ''; + }); + in + + { + default = pkgs.mkShell { + name = "projt-launcher"; + + inputsFrom = [ + packages'.projtlauncher-unwrapped + ]; + + packages = with pkgs; [ + ccache + llvm.clang-tools + ]; + + cmakeBuildType = "Debug"; + cmakeFlags = [ "-GNinja" ] ++ packages'.projtlauncher.cmakeFlags; + dontFixCmake = true; + + shellHook = '' + + echo "Sourcing ${qt-wrapper-env}" + source ${qt-wrapper-env} + + git submodule update --init --force + + if [ ! -f compile_commands.json ]; then + cmakeConfigurePhase + cd .. + ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json + fi + + echo ${lib.escapeShellArg welcomeMessage} + ''; + }; + } + ); + + formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); + + overlays.default = final: prev: { + lib = prev.lib // { + maintainers = prev.lib.maintainers // (import ./nix/maintainers.nix); + }; + + projtlauncher-unwrapped = final.callPackage ./nix/unwrapped.nix { + inherit self; + }; + + projtlauncher = final.callPackage ./nix/wrapper.nix { }; + }; + + packages = forAllSystems ( + system: + + let + pkgs = nixpkgsFor.${system}; + + # Build a scope from our overlay + projtPackages = lib.makeScope pkgs.newScope (final: self.overlays.default final pkgs); + + # Grab our packages from it and set the default + packages = { + inherit (projtPackages) projtlauncher-unwrapped projtlauncher; + default = projtPackages.projtlauncher; + }; + in + + # Only output them if they're available on the current system + packages + ); + + # We put these under legacyPackages as they are meant for CI, not end user consumption + legacyPackages = forAllSystems ( + system: + let + packages' = self.packages.${system}; + in + rec { + projtlauncher-unwrapped-debug = packages'.projtlauncher-unwrapped.overrideAttrs { + cmakeBuildType = "Debug"; + dontStrip = true; + }; + + projtlauncher-debug = packages'.projtlauncher.override { + projtlauncher-unwrapped = projtlauncher-unwrapped-debug; + }; + } + ); + }; +} diff --git a/archived/projt-launcher/flatpak/.editorconfig b/archived/projt-launcher/flatpak/.editorconfig new file mode 100644 index 0000000000..d9e658587f --- /dev/null +++ b/archived/projt-launcher/flatpak/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig specs and documentation: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.yml] +indent_size = 2 + +[*.patch] +indent_style = unset +indent_size = unset +insert_final_newline = unset +trim_trailing_whitespace = unset diff --git a/archived/projt-launcher/flatpak/.gitignore b/archived/projt-launcher/flatpak/.gitignore new file mode 100644 index 0000000000..1e87d7e0e9 --- /dev/null +++ b/archived/projt-launcher/flatpak/.gitignore @@ -0,0 +1,5 @@ +# Flatpak +repo/ +build/ +builddir/ +.flatpak-builder/ diff --git a/archived/projt-launcher/flatpak/.gitmodules b/archived/projt-launcher/flatpak/.gitmodules new file mode 100644 index 0000000000..1e7a990914 --- /dev/null +++ b/archived/projt-launcher/flatpak/.gitmodules @@ -0,0 +1,3 @@ +[submodule "shared-modules"] + path = shared-modules + url = https://github.com/flathub/shared-modules.git diff --git a/archived/projt-launcher/flatpak/README.md b/archived/projt-launcher/flatpak/README.md new file mode 100644 index 0000000000..5313f9825c --- /dev/null +++ b/archived/projt-launcher/flatpak/README.md @@ -0,0 +1,36 @@ +# ProjT Launcher Flatpak Repository + +This is the repository used to publish nightly builds of the [launcher](https://projecttick.org) to our own Flatpak remote at `https://flatpak.projecttick.org`. + +## Usage + +Our repository can be added through its `.flatpakrepo` in most software centers using [this link](https://flatpak.projecttick.org/projtlauncher.flatpakrepo). A `nightly` branch of ProjT Launcher should then appear in search! + +You can also start using it through the CLI: + +```bash +$ flatpak remote-add --if-not-exists projtlauncher https://flatpak.projecttick.org/projtlauncher.flatpakrepo +$ flatpak install org.projecttick.ProjTLauncher//nightly +``` + +### Installing with the Stable Release + +Due to its isolation of apps by default, Flatpak has the nice advantage of allowing you to use the stable and nightly releases of ProjT at the same time! + +To switch between the two, you can use the `flatpak make-current` command: + +```bash +$ # Replace 'nightly' with 'stable' to switch back to the regular release +$ flatpak make-current org.projecttick.ProjTLauncher nightly +``` + +You can also specify which version to run with the `--branch` argument to `flatpak run`: + +```bash +$ flatpak run --branch=nightly org.projecttick.ProjTLauncher +``` + +## Special Thanks + +- [Flatpak's official documentation](https://docs.flatpak.org/en/latest/hosting-a-repository.html) +- [proletarius101's](https://gitlab.com/proletarius101) [GitLab CI template](https://gitlab.com/accessable-net/gitlab-ci-templates) diff --git a/archived/projt-launcher/flatpak/modules/flite.yml b/archived/projt-launcher/flatpak/modules/flite.yml new file mode 100644 index 0000000000..0816970fe6 --- /dev/null +++ b/archived/projt-launcher/flatpak/modules/flite.yml @@ -0,0 +1,15 @@ +name: flite +config-opts: + - --enable-shared + - --with-audio=pulseaudio +no-parallel-make: true +sources: + - type: git + url: https://github.com/festvox/flite.git + tag: v2.2 + commit: e9e2e37c329dbe98bfeb27a1828ef9a71fa84f88 + x-checker-data: + type: git + tag-pattern: ^v([\d.]+)$ + - type: patch + path: ../patches/0001-Ez-peazy.flite.patch diff --git a/archived/projt-launcher/flatpak/modules/glfw.yml b/archived/projt-launcher/flatpak/modules/glfw.yml new file mode 100644 index 0000000000..77453b458f --- /dev/null +++ b/archived/projt-launcher/flatpak/modules/glfw.yml @@ -0,0 +1,27 @@ +name: glfw +buildsystem: cmake-ninja +config-opts: + - -DCMAKE_BUILD_TYPE=RelWithDebInfo + - -DBUILD_SHARED_LIBS:BOOL=ON + - -DGLFW_BUILD_WAYLAND:BOOL=ON + - -DGLFW_BUILD_DOCS:BOOL=OFF +sources: + - type: archive + url: https://github.com/glfw/glfw/archive/3.4.tar.gz + sha256: c038d34200234d071fae9345bc455e4a8f2f544ab60150765d7704e08f3dac01 + x-checker-data: + type: anitya + project-id: 1180 + stable-only: true + url-template: https://github.com/glfw/glfw/archive/$version.tar.gz + + - type: patch + path: ../patches/0001-Wayland-Partially-implement-glfwSetCursorPos.patch + - type: patch + path: ../patches/0002-Wayland-Implement-glfwSetWindowIcon.patch + - type: patch + path: ../patches/0003-proceed-even-though-no-window-icon-support-on-waylan.patch +cleanup: + - /include + - /lib/cmake + - /lib/pkgconfig diff --git a/archived/projt-launcher/flatpak/modules/glxinfo.yml b/archived/projt-launcher/flatpak/modules/glxinfo.yml new file mode 100644 index 0000000000..0be9239ebf --- /dev/null +++ b/archived/projt-launcher/flatpak/modules/glxinfo.yml @@ -0,0 +1,26 @@ +name: glxinfo +buildsystem: meson +config-opts: + - --bindir=/app/mesa-demos + - -Degl=disabled + - -Dglut=disabled + - -Dosmesa=disabled + - -Dvulkan=disabled + - -Dwayland=disabled +post-install: + - mv -v /app/mesa-demos/glxinfo /app/bin +sources: + - type: archive + url: https://archive.mesa3d.org/demos/mesa-demos-9.0.0.tar.xz + sha256: 3046a3d26a7b051af7ebdd257a5f23bfeb160cad6ed952329cdff1e9f1ed496b + x-checker-data: + type: anitya + project-id: 16781 + stable-only: true + url-template: https://archive.mesa3d.org/demos/mesa-demos-$version.tar.xz +cleanup: + - /include + - /mesa-demos + - /share +modules: + - ../shared-modules/glu/glu-9.json diff --git a/archived/projt-launcher/flatpak/modules/inih.yml b/archived/projt-launcher/flatpak/modules/inih.yml new file mode 100644 index 0000000000..4f07097ce0 --- /dev/null +++ b/archived/projt-launcher/flatpak/modules/inih.yml @@ -0,0 +1,6 @@ +name: inih +buildsystem: meson +sources: + - type: git + url: https://github.com/benhoyt/inih + tag: r62 # r62: INIReader::ParseErrorMessage Changelog: https://github.com/benhoyt/inih/compare/r61...r62 diff --git a/archived/projt-launcher/flatpak/modules/xrandr.yml b/archived/projt-launcher/flatpak/modules/xrandr.yml new file mode 100644 index 0000000000..155c630b4f --- /dev/null +++ b/archived/projt-launcher/flatpak/modules/xrandr.yml @@ -0,0 +1,14 @@ +name: xrandr +buildsystem: autotools +sources: + - type: archive + url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz + sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c + x-checker-data: + type: anitya + project-id: 14957 + stable-only: true + url-template: https://xorg.freedesktop.org/archive/individual/app/xrandr-$version.tar.xz +cleanup: + - /share/man + - /bin/xkeystone diff --git a/archived/projt-launcher/flatpak/org.projecttick.ProjTLauncher.yml b/archived/projt-launcher/flatpak/org.projecttick.ProjTLauncher.yml new file mode 100644 index 0000000000..4a1fd000d8 --- /dev/null +++ b/archived/projt-launcher/flatpak/org.projecttick.ProjTLauncher.yml @@ -0,0 +1,82 @@ +id: org.projecttick.ProjTLauncher +runtime: org.kde.Platform +runtime-version: '6.10' +sdk: org.kde.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.openjdk21 + +command: projtlauncher +finish-args: + - --share=ipc + - --socket=x11 + - --socket=wayland + - --device=all + - --share=network + - --socket=pulseaudio + # for Discord RPC mods + - --filesystem=xdg-run/app/com.discordapp.Discord:create + # Mod drag&drop + - --filesystem=xdg-download:ro + # FTBApp import + - --filesystem=~/.ftba:ro + # Userspace visibility for manual hugepages configuration + # Required for -XX:+UseLargePages + - --filesystem=/sys/kernel/mm/hugepages:ro + # Userspace visibility for transparent hugepages configuration + # Required for -XX:+UseTransparentHugePages + - --filesystem=/sys/kernel/mm/transparent_hugepage:ro + +modules: + # Text to Speech in the game + - modules/flite.yml + + # Required for GPU info + - modules/glxinfo.yml + + # Required for older Minecraft versions + - modules/xrandr.yml + + # Our patches improve native Wayland support + - modules/glfw.yml + + # Project Tick Maintained Gamemode version needs inih + - modules/inih.yml + + - name: projtlauncher + buildsystem: cmake-ninja + builddir: true + subdir: projt-launcher + config-opts: + - -DLauncher_BUILD_PLATFORM=flatpak + # This allows us to manage and update Java independently of this Flatpak + - -DLauncher_ENABLE_JAVA_DOWNLOADER=ON + - -DCMAKE_BUILD_TYPE=RelWithDebInfo + # Disable QtWebEngine + - -DLAUNCHER_USE_WEBENGINE=OFF + # Disable Chromium Embedded Framework because Flatpak builds does not support Project Tick Launcher Hub + - -DLAUNCHER_USE_CEF=OFF + # Disable Project Tick Launcher Hub + - -DLAUNCHER_DISABLE_HUB=ON + build-options: + env: + JAVA_HOME: /usr/lib/sdk/openjdk21/jvm/openjdk-21 + JAVA_COMPILER: /usr/lib/sdk/openjdk21/jvm/openjdk-21/bin/javac + run-tests: true + sources: + - type: git + url: https://github.com/Project-Tick/Project-Tick + branch: main + + - name: enhance + buildsystem: simple + build-commands: + - install -Dm755 prime-run /app/bin/prime-run + - mv /app/bin/projtlauncher /app/bin/projtrun + - install -Dm755 projtlauncher /app/bin/projtlauncher + sources: + # Script used to wrap instances and ensure they use dGPUs in hyprid configurations + - type: file + path: scripts/prime-run + # Sets up misc integrations with gamemode, mangohud, Discord RPC, etc. + - type: file + path: scripts/projtlauncher diff --git a/archived/projt-launcher/flatpak/patches/0001-Ez-peazy.flite.patch b/archived/projt-launcher/flatpak/patches/0001-Ez-peazy.flite.patch new file mode 100644 index 0000000000..60cfa411ce --- /dev/null +++ b/archived/projt-launcher/flatpak/patches/0001-Ez-peazy.flite.patch @@ -0,0 +1,25 @@ +From e4b5e40fe29e0fb972ab4abcaa6f0ed137f0fb92 Mon Sep 17 00:00:00 2001 +From: Babakinha <59146844+Babakinha@users.noreply.github.com> +Date: Sat, 3 Sep 2022 19:30:26 -0300 +Subject: [PATCH] Ez peazy + +--- + src/audio/au_alsa.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/audio/au_alsa.c b/src/audio/au_alsa.c +index 93fc706..b8b7c7f 100644 +--- a/src/audio/au_alsa.c ++++ b/src/audio/au_alsa.c +@@ -54,7 +54,7 @@ + #include <alsa/asoundlib.h> + + /*static char *pcm_dev_name = "hw:0,0"; */ +-static const char *pcm_dev_name ="default"; ++static const char *pcm_dev_name ="pipewire"; + + static inline void print_pcm_state(snd_pcm_t *handle, char *msg) + { +-- +2.47.2 + diff --git a/archived/projt-launcher/flatpak/patches/0001-Wayland-Partially-implement-glfwSetCursorPos.patch b/archived/projt-launcher/flatpak/patches/0001-Wayland-Partially-implement-glfwSetCursorPos.patch new file mode 100644 index 0000000000..a3d44e86f0 --- /dev/null +++ b/archived/projt-launcher/flatpak/patches/0001-Wayland-Partially-implement-glfwSetCursorPos.patch @@ -0,0 +1,88 @@ +From 2bcc4749b1ab6819d6dfdb07cd1832ab5d3339bd Mon Sep 17 00:00:00 2001 +From: Friz64 <friz64@protonmail.com> +Date: Sun, 25 Feb 2024 00:51:49 +0100 +Subject: [PATCH 1/3] Wayland: Partially implement `glfwSetCursorPos` + +--- + src/wl_platform.h | 2 ++ + src/wl_window.c | 37 +++++++++++++++++++++++++++++++++++-- + 2 files changed, 37 insertions(+), 2 deletions(-) + +diff --git a/src/wl_platform.h b/src/wl_platform.h +index 149cd241..53b09eef 100644 +--- a/src/wl_platform.h ++++ b/src/wl_platform.h +@@ -371,6 +371,7 @@ typedef struct _GLFWwindowWayland + GLFWbool iconified; + GLFWbool activated; + GLFWbool fullscreen; ++ double cursorPosX, cursorPosY; + } pending; + + struct { +@@ -386,6 +387,7 @@ typedef struct _GLFWwindowWayland + + _GLFWcursor* currentCursor; + double cursorPosX, cursorPosY; ++ GLFWbool pendingCursorPos; + + char* appId; + +diff --git a/src/wl_window.c b/src/wl_window.c +index 5b491ffb..4283f88f 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -2667,8 +2667,34 @@ void _glfwGetCursorPosWayland(_GLFWwindow* window, double* xpos, double* ypos) + + void _glfwSetCursorPosWayland(_GLFWwindow* window, double x, double y) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the cursor position"); ++ if (!_glfw.wl.pointerConstraints) ++ { ++ _glfwInputError(GLFW_FEATURE_UNAVAILABLE, ++ "Wayland: The compositor does not support setting the cursor position"); ++ return; ++ } ++ ++ if (window->wl.lockedPointer) { ++ zwp_locked_pointer_v1_set_cursor_position_hint(window->wl.lockedPointer, ++ wl_fixed_from_double(x), ++ wl_fixed_from_double(y)); ++ } else { ++ if (window->cursorMode != GLFW_CURSOR_DISABLED) { ++ _glfwInputError(GLFW_PLATFORM_ERROR, ++ "Wayland: Delaying the cursor position update until " ++ "the cursor mode is set to GLFW_CURSOR_DISABLED"); ++ } ++ ++ // The cursor is not currently locked, but it may be locked later. Either ++ // - the application has already set the cursor mode to GLFW_CURSOR_DISABLED, ++ // but the cursor is currently outside of the window, or ++ // - the application has not yet set the cursor mode to GLFW_CURSOR_DISABLED, ++ // but will do so soon. ++ // Defer setting the cursor position to _glfwSetCursorWayland. ++ window->wl.pending.cursorPosX = x; ++ window->wl.pending.cursorPosY = y; ++ window->wl.pendingCursorPos = GLFW_TRUE; ++ } + } + + void _glfwSetCursorModeWayland(_GLFWwindow* window, int mode) +@@ -3009,6 +3035,13 @@ void _glfwSetCursorWayland(_GLFWwindow* window, _GLFWcursor* cursor) + unconfinePointer(window); + if (!window->wl.lockedPointer) + lockPointer(window); ++ ++ if (window->wl.pendingCursorPos == GLFW_TRUE) { ++ zwp_locked_pointer_v1_set_cursor_position_hint(window->wl.lockedPointer, ++ wl_fixed_from_double(window->wl.pending.cursorPosX), ++ wl_fixed_from_double(window->wl.pending.cursorPosY)); ++ window->wl.pendingCursorPos = GLFW_FALSE; ++ } + } + else if (window->cursorMode == GLFW_CURSOR_CAPTURED) + { +-- +2.52.0 + diff --git a/archived/projt-launcher/flatpak/patches/0002-Wayland-Implement-glfwSetWindowIcon.patch b/archived/projt-launcher/flatpak/patches/0002-Wayland-Implement-glfwSetWindowIcon.patch new file mode 100644 index 0000000000..199880902a --- /dev/null +++ b/archived/projt-launcher/flatpak/patches/0002-Wayland-Implement-glfwSetWindowIcon.patch @@ -0,0 +1,386 @@ +From 823a4c7a61029fe16257615f55f42721fe850841 Mon Sep 17 00:00:00 2001 +From: JakobDev <jakobdev@gmx.de> +Date: Wed, 12 Feb 2025 19:24:19 +0100 +Subject: [PATCH 2/3] Wayland: Implement glfwSetWindowIcon + +--- + deps/wayland/xdg-toplevel-icon-v1.xml | 205 ++++++++++++++++++++++++++ + include/GLFW/glfw3.h | 6 +- + src/CMakeLists.txt | 1 + + src/wl_init.c | 14 ++ + src/wl_platform.h | 1 + + src/wl_window.c | 52 ++++++- + 6 files changed, 274 insertions(+), 5 deletions(-) + create mode 100644 deps/wayland/xdg-toplevel-icon-v1.xml + +diff --git a/deps/wayland/xdg-toplevel-icon-v1.xml b/deps/wayland/xdg-toplevel-icon-v1.xml +new file mode 100644 +index 00000000..fc409fef +--- /dev/null ++++ b/deps/wayland/xdg-toplevel-icon-v1.xml +@@ -0,0 +1,205 @@ ++<?xml version="1.0" encoding="UTF-8"?> ++<protocol name="xdg_toplevel_icon_v1"> ++ ++ <copyright> ++ Copyright © 2023-2024 Matthias Klumpp ++ Copyright © 2024 David Edmundson ++ ++ Permission is hereby granted, free of charge, to any person obtaining a ++ copy of this software and associated documentation files (the "Software"), ++ to deal in the Software without restriction, including without limitation ++ the rights to use, copy, modify, merge, publish, distribute, sublicense, ++ and/or sell copies of the Software, and to permit persons to whom the ++ Software is furnished to do so, subject to the following conditions: ++ ++ The above copyright notice and this permission notice (including the next ++ paragraph) shall be included in all copies or substantial portions of the ++ Software. ++ ++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL ++ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ++ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER ++ DEALINGS IN THE SOFTWARE. ++ </copyright> ++ ++ <description summary="protocol to assign icons to toplevels"> ++ This protocol allows clients to set icons for their toplevel surfaces ++ either via the XDG icon stock (using an icon name), or from pixel data. ++ ++ A toplevel icon represents the individual toplevel (unlike the application ++ or launcher icon, which represents the application as a whole), and may be ++ shown in window switchers, window overviews and taskbars that list ++ individual windows. ++ ++ This document adheres to RFC 2119 when using words like "must", ++ "should", "may", etc. ++ ++ Warning! The protocol described in this file is currently in the testing ++ phase. Backward compatible changes may be added together with the ++ corresponding interface version bump. Backward incompatible changes can ++ only be done by creating a new major version of the extension. ++ </description> ++ ++ <interface name="xdg_toplevel_icon_manager_v1" version="1"> ++ <description summary="interface to manage toplevel icons"> ++ This interface allows clients to create toplevel window icons and set ++ them on toplevel windows to be displayed to the user. ++ </description> ++ ++ <request name="destroy" type="destructor"> ++ <description summary="destroy the toplevel icon manager"> ++ Destroy the toplevel icon manager. ++ This does not destroy objects created with the manager. ++ </description> ++ </request> ++ ++ <request name="create_icon"> ++ <description summary="create a new icon instance"> ++ Creates a new icon object. This icon can then be attached to a ++ xdg_toplevel via the 'set_icon' request. ++ </description> ++ <arg name="id" type="new_id" interface="xdg_toplevel_icon_v1"/> ++ </request> ++ ++ <request name="set_icon"> ++ <description summary="set an icon on a toplevel window"> ++ This request assigns the icon 'icon' to 'toplevel', or clears the ++ toplevel icon if 'icon' was null. ++ This state is double-buffered and is applied on the next ++ wl_surface.commit of the toplevel. ++ ++ After making this call, the xdg_toplevel_icon_v1 provided as 'icon' ++ can be destroyed by the client without 'toplevel' losing its icon. ++ The xdg_toplevel_icon_v1 is immutable from this point, and any ++ future attempts to change it must raise the ++ 'xdg_toplevel_icon_v1.immutable' protocol error. ++ ++ The compositor must set the toplevel icon from either the pixel data ++ the icon provides, or by loading a stock icon using the icon name. ++ See the description of 'xdg_toplevel_icon_v1' for details. ++ ++ If 'icon' is set to null, the icon of the respective toplevel is reset ++ to its default icon (usually the icon of the application, derived from ++ its desktop-entry file, or a placeholder icon). ++ If this request is passed an icon with no pixel buffers or icon name ++ assigned, the icon must be reset just like if 'icon' was null. ++ </description> ++ <arg name="toplevel" type="object" interface="xdg_toplevel" summary="the toplevel to act on"/> ++ <arg name="icon" type="object" interface="xdg_toplevel_icon_v1" allow-null="true"/> ++ </request> ++ ++ <event name="icon_size"> ++ <description summary="describes a supported & preferred icon size"> ++ This event indicates an icon size the compositor prefers to be ++ available if the client has scalable icons and can render to any size. ++ ++ When the 'xdg_toplevel_icon_manager_v1' object is created, the ++ compositor may send one or more 'icon_size' events to describe the list ++ of preferred icon sizes. If the compositor has no size preference, it ++ may not send any 'icon_size' event, and it is up to the client to ++ decide a suitable icon size. ++ ++ A sequence of 'icon_size' events must be finished with a 'done' event. ++ If the compositor has no size preferences, it must still send the ++ 'done' event, without any preceding 'icon_size' events. ++ </description> ++ <arg name="size" type="int" ++ summary="the edge size of the square icon in surface-local coordinates, e.g. 64"/> ++ </event> ++ ++ <event name="done"> ++ <description summary="all information has been sent"> ++ This event is sent after all 'icon_size' events have been sent. ++ </description> ++ </event> ++ </interface> ++ ++ <interface name="xdg_toplevel_icon_v1" version="1"> ++ <description summary="a toplevel window icon"> ++ This interface defines a toplevel icon. ++ An icon can have a name, and multiple buffers. ++ In order to be applied, the icon must have either a name, or at least ++ one buffer assigned. Applying an empty icon (with no buffer or name) to ++ a toplevel should reset its icon to the default icon. ++ ++ It is up to compositor policy whether to prefer using a buffer or loading ++ an icon via its name. See 'set_name' and 'add_buffer' for details. ++ </description> ++ ++ <enum name="error"> ++ <entry name="invalid_buffer" ++ summary="the provided buffer does not satisfy requirements" ++ value="1"/> ++ <entry name="immutable" ++ summary="the icon has already been assigned to a toplevel and must not be changed" ++ value="2"/> ++ <entry name="no_buffer" ++ summary="the provided buffer has been destroyed before the toplevel icon" ++ value="3"/> ++ </enum> ++ ++ <request name="destroy" type="destructor"> ++ <description summary="destroy the icon object"> ++ Destroys the 'xdg_toplevel_icon_v1' object. ++ The icon must still remain set on every toplevel it was assigned to, ++ until the toplevel icon is reset explicitly. ++ </description> ++ </request> ++ ++ <request name="set_name"> ++ <description summary="set an icon name"> ++ This request assigns an icon name to this icon. ++ Any previously set name is overridden. ++ ++ The compositor must resolve 'icon_name' according to the lookup rules ++ described in the XDG icon theme specification[1] using the ++ environment's current icon theme. ++ ++ If the compositor does not support icon names or cannot resolve ++ 'icon_name' according to the XDG icon theme specification it must ++ fall back to using pixel buffer data instead. ++ ++ If this request is made after the icon has been assigned to a toplevel ++ via 'set_icon', a 'immutable' error must be raised. ++ ++ [1]: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html ++ </description> ++ <arg name="icon_name" type="string"/> ++ </request> ++ ++ <request name="add_buffer"> ++ <description summary="add icon data from a pixel buffer"> ++ This request adds pixel data supplied as wl_buffer to the icon. ++ ++ The client should add pixel data for all icon sizes and scales that ++ it can provide, or which are explicitly requested by the compositor ++ via 'icon_size' events on xdg_toplevel_icon_manager_v1. ++ ++ The wl_buffer supplying pixel data as 'buffer' must be backed by wl_shm ++ and must be a square (width and height being equal). ++ If any of these buffer requirements are not fulfilled, a 'invalid_buffer' ++ error must be raised. ++ ++ If this icon instance already has a buffer of the same size and scale ++ from a previous 'add_buffer' request, data from the last request ++ overrides the preexisting pixel data. ++ ++ The wl_buffer must be kept alive for as long as the xdg_toplevel_icon ++ it is associated with is not destroyed, otherwise a 'no_buffer' error ++ is raised. The buffer contents must not be modified after it was ++ assigned to the icon. As a result, the region of the wl_shm_pool's ++ backing storage used for the wl_buffer must not be modified after this ++ request is sent. The wl_buffer.release event is unused. ++ ++ If this request is made after the icon has been assigned to a toplevel ++ via 'set_icon', a 'immutable' error must be raised. ++ </description> ++ <arg name="buffer" type="object" interface="wl_buffer"/> ++ <arg name="scale" type="int" ++ summary="the scaling factor of the icon, e.g. 1"/> ++ </request> ++ </interface> ++</protocol> +diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h +index 9c55ac9d..c49839c2 100644 +--- a/include/GLFW/glfw3.h ++++ b/include/GLFW/glfw3.h +@@ -3398,9 +3398,9 @@ GLFWAPI void glfwSetWindowTitle(GLFWwindow* window, const char* title); + * + * [bundle-guide]: https://developer.apple.com/library/mac/documentation/CoreFoundation/Conceptual/CFBundles/ + * +- * @remark @wayland There is no existing protocol to change an icon, the +- * window will thus inherit the one defined in the application's desktop file. +- * This function will emit @ref GLFW_FEATURE_UNAVAILABLE. ++ * @remark @wayland This only works on compositors implementing the XDG toplevel icon protocol. ++ * This function will emit @ref GLFW_FEATURE_UNAVAILABLE if this protocol is not available. ++ * The icon must be square. Otherwise @ref GLFW_INVALID_VALUE will be emited. + * + * @thread_safety This function must only be called from the main thread. + * +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index 1057a6f9..ba200275 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -104,6 +104,7 @@ if (GLFW_BUILD_WAYLAND) + generate_wayland_protocol("fractional-scale-v1.xml") + generate_wayland_protocol("xdg-activation-v1.xml") + generate_wayland_protocol("xdg-decoration-unstable-v1.xml") ++ generate_wayland_protocol("xdg-toplevel-icon-v1.xml") + endif() + + if (WIN32 AND GLFW_BUILD_SHARED_LIBRARY) +diff --git a/src/wl_init.c b/src/wl_init.c +index 3aff476d..064080c7 100644 +--- a/src/wl_init.c ++++ b/src/wl_init.c +@@ -49,6 +49,7 @@ + #include "fractional-scale-v1-client-protocol.h" + #include "xdg-activation-v1-client-protocol.h" + #include "idle-inhibit-unstable-v1-client-protocol.h" ++#include "xdg-toplevel-icon-v1-client-protocol.h" + + // NOTE: Versions of wayland-scanner prior to 1.17.91 named every global array of + // wl_interface pointers 'types', making it impossible to combine several unmodified +@@ -91,6 +92,10 @@ + #include "idle-inhibit-unstable-v1-client-protocol-code.h" + #undef types + ++#define types _glfw_toplevel_icon_types ++#include "xdg-toplevel-icon-v1-client-protocol-code.h" ++#undef types ++ + static void wmBaseHandlePing(void* userData, + struct xdg_wm_base* wmBase, + uint32_t serial) +@@ -201,6 +206,13 @@ static void registryHandleGlobal(void* userData, + &wp_fractional_scale_manager_v1_interface, + 1); + } ++ else if (strcmp(interface, xdg_toplevel_icon_manager_v1_interface.name) == 0) ++ { ++ _glfw.wl.toplevelIconManager = ++ wl_registry_bind(registry, name, ++ &xdg_toplevel_icon_manager_v1_interface, ++ 1); ++ } + } + + static void registryHandleGlobalRemove(void* userData, +@@ -983,6 +995,8 @@ void _glfwTerminateWayland(void) + xdg_activation_v1_destroy(_glfw.wl.activationManager); + if (_glfw.wl.fractionalScaleManager) + wp_fractional_scale_manager_v1_destroy(_glfw.wl.fractionalScaleManager); ++ if (_glfw.wl.toplevelIconManager) ++ xdg_toplevel_icon_manager_v1_destroy(_glfw.wl.toplevelIconManager); + if (_glfw.wl.registry) + wl_registry_destroy(_glfw.wl.registry); + if (_glfw.wl.display) +diff --git a/src/wl_platform.h b/src/wl_platform.h +index 53b09eef..347b20c8 100644 +--- a/src/wl_platform.h ++++ b/src/wl_platform.h +@@ -439,6 +439,7 @@ typedef struct _GLFWlibraryWayland + struct zwp_idle_inhibit_manager_v1* idleInhibitManager; + struct xdg_activation_v1* activationManager; + struct wp_fractional_scale_manager_v1* fractionalScaleManager; ++ struct xdg_toplevel_icon_manager_v1* toplevelIconManager; + + _GLFWofferWayland* offers; + unsigned int offerCount; +diff --git a/src/wl_window.c b/src/wl_window.c +index 4283f88f..0e67e5c5 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -51,6 +51,7 @@ + #include "xdg-activation-v1-client-protocol.h" + #include "idle-inhibit-unstable-v1-client-protocol.h" + #include "fractional-scale-v1-client-protocol.h" ++#include "xdg-toplevel-icon-v1-client-protocol.h" + + #define GLFW_BORDER_SIZE 4 + #define GLFW_CAPTION_HEIGHT 24 +@@ -2227,8 +2228,55 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title) + void _glfwSetWindowIconWayland(_GLFWwindow* window, + int count, const GLFWimage* images) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the window icon"); ++ if (!_glfw.wl.toplevelIconManager) ++ { ++ _glfwInputError(GLFW_FEATURE_UNAVAILABLE, ++ "Wayland: The platform does not support setting the window icon"); ++ return; ++ } ++ ++ if (!count) ++ { ++ if (window->wl.libdecor.frame) ++ xdg_toplevel_icon_manager_v1_set_icon(_glfw.wl.toplevelIconManager, ++ libdecor_frame_get_xdg_toplevel(window->wl.libdecor.frame), ++ NULL); ++ else if (window->wl.xdg.toplevel) ++ xdg_toplevel_icon_manager_v1_set_icon(_glfw.wl.toplevelIconManager, window->wl.xdg.toplevel, NULL); ++ return; ++ } ++ ++ for (int i = 0; i < count; i++) ++ { ++ if (images[i].width != images[i].height) ++ { ++ _glfwInputError(GLFW_INVALID_VALUE, ++ "Wayland: The icon must be a square"); ++ return; ++ } ++ } ++ ++ struct xdg_toplevel_icon_v1 *icon = xdg_toplevel_icon_manager_v1_create_icon(_glfw.wl.toplevelIconManager); ++ struct wl_buffer *bufferArr[count]; ++ ++ for (int i = 0; i < count; i++) ++ { ++ bufferArr[i] = createShmBuffer(&images[i]); ++ xdg_toplevel_icon_v1_add_buffer(icon, bufferArr[i], 1); ++ } ++ ++ if (window->wl.libdecor.frame) ++ xdg_toplevel_icon_manager_v1_set_icon(_glfw.wl.toplevelIconManager, ++ libdecor_frame_get_xdg_toplevel(window->wl.libdecor.frame), ++ icon); ++ else if (window->wl.xdg.toplevel) ++ xdg_toplevel_icon_manager_v1_set_icon(_glfw.wl.toplevelIconManager, window->wl.xdg.toplevel, icon); ++ xdg_toplevel_icon_v1_destroy(icon); ++ ++ for (int i = 0; i < count; i++) ++ { ++ wl_buffer_destroy(bufferArr[i]); ++ } + } + + void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos) +-- +2.52.0 + diff --git a/archived/projt-launcher/flatpak/patches/0003-proceed-even-though-no-window-icon-support-on-waylan.patch b/archived/projt-launcher/flatpak/patches/0003-proceed-even-though-no-window-icon-support-on-waylan.patch new file mode 100644 index 0000000000..9fc21aaff8 --- /dev/null +++ b/archived/projt-launcher/flatpak/patches/0003-proceed-even-though-no-window-icon-support-on-waylan.patch @@ -0,0 +1,30 @@ +From 8f4a285cbf4f66b14f03b992979d7b8469b95069 Mon Sep 17 00:00:00 2001 +From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> +Date: Mon, 5 Jan 2026 20:05:47 +0100 +Subject: [PATCH 3/3] proceed even though no window icon support on wayland + +hack needed for old versions of minecraft with our custom glfw and most compositors. Thanks to BoyOrigin/glfw-wayland for the original implementation + +Co-Authored-By: FayBoy <ahmadyasinfikri@gmail.com> +--- + src/wl_window.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/wl_window.c b/src/wl_window.c +index 0e67e5c5..cddcac44 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -2230,8 +2230,8 @@ void _glfwSetWindowIconWayland(_GLFWwindow* window, + { + if (!_glfw.wl.toplevelIconManager) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the window icon"); ++ fprintf(stderr, ++ "[GLFW] Wayland: The compositor does not support setting the window icon"); + return; + } + +-- +2.52.0 + diff --git a/archived/projt-launcher/flatpak/pubkey.asc b/archived/projt-launcher/flatpak/pubkey.asc new file mode 100644 index 0000000000..db811e3d8a --- /dev/null +++ b/archived/projt-launcher/flatpak/pubkey.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaSC/1BYJKwYBBAHaRw8BAQdABmQgHkYFfgFfY+6aOYz0aiuEMFK74p1YbJtT +xuiwrKa0IllvbmcgRG8tSHl1biA8ZnJvc3RlcjEyQG5hdmVyLmNvbT6ImQQTFgoA +QRYhBPtWUnLVrotXRj2pZeSbU8kYwzmmBQJpIL/UAhsDBQkFpFY8BQsJCAcCAiIC +BhUKCQgLAgQWAgMBAh4HAheAAAoJEOSbU8kYwzmmZigA/1LV5AF9IijBArMtaHBX +G2gXiiemythoQYeB1B56KICSAP9eSK3L9BlImSbeyrB4fTJ+zNAH2O54lVN+1nYt +ZFlmBrg4BGkgv9QSCisGAQQBl1UBBQEBB0CqNxm5eYMxtysVyW4DZTGNQSEPN2oh +T9crxVEBPqF/AwMBCAeIfgQYFgoAJhYhBPtWUnLVrotXRj2pZeSbU8kYwzmmBQJp +IL/UAhsMBQkFpFY8AAoJEOSbU8kYwzmmleEBAIf3BcYpa4nIcll3HiVjIbPAfmfa +pH55zn1HpyRyokl3AP9FJrUx9IMc8ktHkqHgylVIma9KdbNuFjh+odOwFkZDDw== +=FxU6 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/archived/projt-launcher/flatpak/scripts/prime-run b/archived/projt-launcher/flatpak/scripts/prime-run new file mode 100644 index 0000000000..946c28dd59 --- /dev/null +++ b/archived/projt-launcher/flatpak/scripts/prime-run @@ -0,0 +1,4 @@ +#!/bin/sh + +export __NV_PRIME_RENDER_OFFLOAD=1 __VK_LAYER_NV_optimus=NVIDIA_only __GLX_VENDOR_LIBRARY_NAME=nvidia +exec "$@" diff --git a/archived/projt-launcher/flatpak/scripts/projtlauncher b/archived/projt-launcher/flatpak/scripts/projtlauncher new file mode 100644 index 0000000000..90ea33e4e4 --- /dev/null +++ b/archived/projt-launcher/flatpak/scripts/projtlauncher @@ -0,0 +1,11 @@ +#!/bin/bash + +# discord RPC +for i in {0..9}; do + test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i"; +done + +export PATH="${PATH}${PATH:+:}/usr/lib/extensions/vulkan/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin" +export VK_LAYER_PATH="/usr/lib/extensions/vulkan/share/vulkan/implicit_layer.d/" + +exec /app/bin/projtrun "$@" diff --git a/archived/projt-launcher/flatpak/static/index.html b/archived/projt-launcher/flatpak/static/index.html new file mode 100644 index 0000000000..da88be7be4 --- /dev/null +++ b/archived/projt-launcher/flatpak/static/index.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <!-- Quick and dirty CSS sourced from the main site! --> + <style> + :root { font-family: system-ui, sans-serif; } + body { background-color: #15181c; } + a { color: hsl(214, 46%, 52%); } + h1, p { color: #ffffff; } + </style> + + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>ProjT Launcher Flatpak Repository + + +

ProjT Launcher Flatpak Repository

+

+ You probably don't want to be on this page. + Check out our website at projecttick.org! +

+ + diff --git a/archived/projt-launcher/flatpak/static/projtlauncher-nightly.flatpakref b/archived/projt-launcher/flatpak/static/projtlauncher-nightly.flatpakref new file mode 100644 index 0000000000..0d448a4e61 --- /dev/null +++ b/archived/projt-launcher/flatpak/static/projtlauncher-nightly.flatpakref @@ -0,0 +1,8 @@ +[Flatpak Ref] +Name=org.projecttick.ProjTLauncher +Branch=nightly +Title=org.projecttick.ProjTLauncher from the official nightly repository +Url=https://flatpak.projecttick.org/ +RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo +IsRuntime=false +GPGKey=mDMEaSC/1BYJKwYBBAHaRw8BAQdABmQgHkYFfgFfY+6aOYz0aiuEMFK74p1YbJtTxuiwrKa0IllvbmcgRG8tSHl1biA8ZnJvc3RlcjEyQG5hdmVyLmNvbT6ImQQTFgoAQRYhBPtWUnLVrotXRj2pZeSbU8kYwzmmBQJpIL/UAhsDBQkFpFY8BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEOSbU8kYwzmmZigA/1LV5AF9IijBArMtaHBXG2gXiiemythoQYeB1B56KICSAP9eSK3L9BlImSbeyrB4fTJ+zNAH2O54lVN+1nYtZFlmBrg4BGkgv9QSCisGAQQBl1UBBQEBB0CqNxm5eYMxtysVyW4DZTGNQSEPN2ohT9crxVEBPqF/AwMBCAeIfgQYFgoAJhYhBPtWUnLVrotXRj2pZeSbU8kYwzmmBQJpIL/UAhsMBQkFpFY8AAoJEOSbU8kYwzmmleEBAIf3BcYpa4nIcll3HiVjIbPAfmfapH55zn1HpyRyokl3AP9FJrUx9IMc8ktHkqHgylVIma9KdbNuFjh+odOwFkZDDw== diff --git a/archived/projt-launcher/flatpak/static/projtlauncher.flatpakrepo b/archived/projt-launcher/flatpak/static/projtlauncher.flatpakrepo new file mode 100644 index 0000000000..75a68535a5 --- /dev/null +++ b/archived/projt-launcher/flatpak/static/projtlauncher.flatpakrepo @@ -0,0 +1,8 @@ +[Flatpak Repo] +Title=ProjT Launcher +Url=https://flatpak.projecttick.org/ +Homepage=https://projecttick.org/ +Comment=Official repository for nightly builds of ProjT Launcher +Description=Official repository for nightly builds of ProjT Launcher +Icon=https://projecttick.org/img/logo.svg +GPGKey=mDMEaSC/1BYJKwYBBAHaRw8BAQdABmQgHkYFfgFfY+6aOYz0aiuEMFK74p1YbJtTxuiwrKa0IllvbmcgRG8tSHl1biA8ZnJvc3RlcjEyQG5hdmVyLmNvbT6ImQQTFgoAQRYhBPtWUnLVrotXRj2pZeSbU8kYwzmmBQJpIL/UAhsDBQkFpFY8BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEOSbU8kYwzmmZigA/1LV5AF9IijBArMtaHBXG2gXiiemythoQYeB1B56KICSAP9eSK3L9BlImSbeyrB4fTJ+zNAH2O54lVN+1nYtZFlmBrg4BGkgv9QSCisGAQQBl1UBBQEBB0CqNxm5eYMxtysVyW4DZTGNQSEPN2ohT9crxVEBPqF/AwMBCAeIfgQYFgoAJhYhBPtWUnLVrotXRj2pZeSbU8kYwzmmBQJpIL/UAhsMBQkFpFY8AAoJEOSbU8kYwzmmleEBAIf3BcYpa4nIcll3HiVjIbPAfmfapH55zn1HpyRyokl3AP9FJrUx9IMc8ktHkqHgylVIma9KdbNuFjh+odOwFkZDDw== \ No newline at end of file diff --git a/archived/projt-launcher/fuzz/CMakeLists.txt b/archived/projt-launcher/fuzz/CMakeLists.txt new file mode 100644 index 0000000000..8828726a7b --- /dev/null +++ b/archived/projt-launcher/fuzz/CMakeLists.txt @@ -0,0 +1,39 @@ +set(FUZZ_ENGINE "") +if(DEFINED ENV{LIB_FUZZING_ENGINE}) + set(FUZZ_ENGINE $ENV{LIB_FUZZING_ENGINE}) +endif() + +function(add_fuzzer target) + add_executable(${target} ${ARGN}) + target_compile_features(${target} PRIVATE cxx_std_20) + if(FUZZ_ENGINE) + target_link_libraries(${target} PRIVATE ${FUZZ_ENGINE}) + else() + target_compile_options(${target} PRIVATE -fsanitize=fuzzer) + target_link_options(${target} PRIVATE -fsanitize=fuzzer) + endif() +endfunction() + +add_fuzzer(fuzz_nbt_reader fuzz_nbt_reader.cpp) +target_link_libraries(fuzz_nbt_reader PRIVATE nbt++) + +# Disabled: Qt6 adds unnecessary glib dependency in fuzzing environment +# add_fuzzer(fuzz_qjson_parse fuzz_qjson_parse.cpp) +# target_link_libraries(fuzz_qjson_parse PRIVATE Qt6::Core) + +# fuzz_gzip requires Qt6::Core for QByteArray/QFile usage in GZip.cpp +# Only build if Qt6::Core is available +if(TARGET Qt6::Core) + add_fuzzer(fuzz_gzip fuzz_gzip.cpp ${PROJECT_SOURCE_DIR}/launcher/GZip.cpp) + target_include_directories(fuzz_gzip PRIVATE ${PROJECT_SOURCE_DIR}/launcher) + # Use static ptlibzippy for fuzzer to avoid sanitizer symbol mismatch + if(TARGET ptlibzippystatic) + target_link_libraries(fuzz_gzip PRIVATE Qt6::Core ptlibzippystatic) + else() + target_link_libraries(fuzz_gzip PRIVATE Qt6::Core PTlibzippy::PTlibzippy) + endif() + + add_fuzzer(fuzz_separator_prefix_tree fuzz_separator_prefix_tree.cpp) + target_include_directories(fuzz_separator_prefix_tree PRIVATE ${PROJECT_SOURCE_DIR}/launcher) + target_link_libraries(fuzz_separator_prefix_tree PRIVATE Qt6::Core) +endif() diff --git a/archived/projt-launcher/fuzz/fuzz_gzip.cpp b/archived/projt-launcher/fuzz/fuzz_gzip.cpp new file mode 100644 index 0000000000..8db3c716f4 --- /dev/null +++ b/archived/projt-launcher/fuzz/fuzz_gzip.cpp @@ -0,0 +1,17 @@ +#include + +#include + +#include "GZip.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + QByteArray input(reinterpret_cast(data), static_cast(size)); + QByteArray output; + QByteArray roundtrip; + + GZip::unzip(input, output); + GZip::zip(output, roundtrip); + + return 0; +} diff --git a/archived/projt-launcher/fuzz/fuzz_nbt_reader.cpp b/archived/projt-launcher/fuzz/fuzz_nbt_reader.cpp new file mode 100644 index 0000000000..d6b1291c6f --- /dev/null +++ b/archived/projt-launcher/fuzz/fuzz_nbt_reader.cpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include + +#include "io/stream_reader.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + // Avoid excessive allocations on pathological inputs + // Very strict limit to prevent array allocation bombs + // E.g., NBT array with count 0x92929292 would allocate 3.2GB + constexpr size_t kMaxInputSize = 4 * 1024; // 4 KiB (was 64 KiB) + if (!data || size == 0 || size > kMaxInputSize) + { + return 0; + } + + try + { + // Use custom string stream without extra copies + std::istringstream stream(std::string(reinterpret_cast(data), size), std::ios::binary); + nbt::io::read_compound(stream); + } + catch (const std::exception&) + { + // Expected for malformed inputs or resource exhaustion + } + catch (const std::bad_alloc&) + { + // Handle out-of-memory gracefully + return -1; + } + + return 0; +} diff --git a/archived/projt-launcher/fuzz/fuzz_qjson_parse.cpp b/archived/projt-launcher/fuzz/fuzz_qjson_parse.cpp new file mode 100644 index 0000000000..ee4db92ed6 --- /dev/null +++ b/archived/projt-launcher/fuzz/fuzz_qjson_parse.cpp @@ -0,0 +1,17 @@ +#include + +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (!data || size == 0) + { + return 0; + } + + QJsonParseError error{}; + QJsonDocument::fromJson(QByteArray(reinterpret_cast(data), static_cast(size)), &error); + + return 0; +} diff --git a/archived/projt-launcher/fuzz/fuzz_separator_prefix_tree.cpp b/archived/projt-launcher/fuzz/fuzz_separator_prefix_tree.cpp new file mode 100644 index 0000000000..d50f616050 --- /dev/null +++ b/archived/projt-launcher/fuzz/fuzz_separator_prefix_tree.cpp @@ -0,0 +1,123 @@ +#include +#include +#include + +#include +#include +#include + +#include "SeparatorPrefixTree.h" + +namespace { +class InputCursor +{ + public: + InputCursor(const uint8_t* data, size_t size) : m_data(data), m_size(size) {} + + bool has(size_t count) const + { + return m_pos + count <= m_size; + } + + uint8_t nextByte() + { + if (!has(1)) + return 0; + return m_data[m_pos++]; + } + + QByteArray takeBytes(size_t count) + { + if (!has(count)) + count = m_size - m_pos; + QByteArray out(reinterpret_cast(m_data + m_pos), static_cast(count)); + m_pos += count; + return out; + } + + QString nextPath() + { + static const char kAlphabet[] = "abcdefghijklmnopqrstuvwxyz0123456789-_."; + const int alphabet_len = static_cast(sizeof(kAlphabet) - 1); + + const int segments = 1 + (nextByte() % 4); + QStringList parts; + parts.reserve(segments); + + for (int i = 0; i < segments; ++i) { + const int length = 1 + (nextByte() % 8); + QByteArray bytes; + bytes.reserve(length); + for (int j = 0; j < length; ++j) { + bytes.append(kAlphabet[nextByte() % alphabet_len]); + } + parts.append(QString::fromLatin1(bytes)); + } + + return parts.join('/'); + } + + private: + const uint8_t* m_data = nullptr; + size_t m_size = 0; + size_t m_pos = 0; +}; +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + InputCursor cursor(data, size); + SeparatorPrefixTree<'/'> tree; + + const size_t max_ops = size < 256 ? size : 256; + for (size_t i = 0; i < max_ops && cursor.has(1); ++i) { + const uint8_t op = cursor.nextByte() % 8; + const QString path = cursor.nextPath(); + + switch (op) { + case 0: + tree.insert(path); + break; + case 1: + tree.remove(path); + break; + case 2: { + const bool contains = tree.contains(path); + if (contains && !tree.covers(path)) { + abort(); + } + break; + } + case 3: + tree.covers(path); + break; + case 4: { + const bool covers = tree.covers(path); + const QString cover = tree.cover(path); + if (covers && cover.isNull()) { + abort(); + } + break; + } + case 5: + tree.exists(path); + break; + case 6: { + const auto list = tree.toStringList(); + for (const auto& entry : list) { + if (!tree.contains(entry)) { + abort(); + } + } + break; + } + case 7: + tree.clear(); + break; + default: + break; + } + } + + return 0; +} diff --git a/archived/projt-launcher/garnix.yaml b/archived/projt-launcher/garnix.yaml new file mode 100644 index 0000000000..312815da5d --- /dev/null +++ b/archived/projt-launcher/garnix.yaml @@ -0,0 +1,5 @@ +builds: + include: + - "checks.x86_64-linux.*" + - "devShells.*.*" + - "packages.*.*" diff --git a/archived/projt-launcher/launcher/Application.cpp b/archived/projt-launcher/launcher/Application.cpp new file mode 100644 index 0000000000..cb878e5bc0 --- /dev/null +++ b/archived/projt-launcher/launcher/Application.cpp @@ -0,0 +1,2450 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Lenny McLennington + * Copyright (C) 2022 Tayou + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ======================================================================== */ + +#include "Application.h" +#include "BuildConfig.h" + +#include "DataMigrationTask.h" +#include "java/services/RuntimeCatalog.hpp" +#include "minecraft/BackupManager.h" +#include "net/PasteUpload.h" +#include "tasks/Task.h" +#include "tools/GenericProfiler.h" +#include "ui/InstanceWindow.h" +#include "ui/MainWindow.h" +#include "ui/ViewLogWindow.h" + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/instanceview/AccessibleInstanceView.h" + +#include + +#include "ui/pages/BasePageProvider.h" +#include "ui/pages/global/APIPage.h" +#include "ui/pages/global/AccountListPage.h" +#include "ui/pages/global/AppearancePage.h" +#include "ui/pages/global/ExternalToolsPage.h" +#include "ui/pages/global/JavaPage.h" +#include "ui/pages/global/LanguagePage.h" +#include "ui/pages/global/LauncherPage.h" +#include "ui/pages/global/MinecraftPage.h" +#include "ui/pages/global/ProxyPage.h" + +#include "ui/setupwizard/AutoJavaWizardPage.h" +#include "ui/setupwizard/JavaWizardPage.h" +#include "ui/setupwizard/LanguageWizardPage.h" +#include "ui/setupwizard/LoginWizardPage.h" +#include "ui/setupwizard/PasteWizardPage.h" +#include "ui/setupwizard/SearchWizardPage.h" +#include "ui/setupwizard/SetupWizard.h" +#include "ui/setupwizard/ThemeWizardPage.h" + +#include "ui/dialogs/CustomMessageBox.h" + +#include "ui/pagedialog/PageDialog.h" + +#include "ui/themes/ThemeManager.h" + +#include "ApplicationMessage.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "InstanceList.h" +#include "MTPixmapCache.h" + +#include +#include "icons/IconList.hpp" +#include "net/HttpMetaCache.h" + +#include "java/services/RuntimeCatalog.hpp" + +#include "updater/ExternalUpdater.h" + +#include "tools/JProfiler.h" +#include "tools/JVisualVM.h" +#include "tools/MCEditTool.h" + +#include "settings/INISettingsObject.h" +#include "settings/Setting.h" + +#include "meta/Index.hpp" +#include "translations/TranslationsModel.h" + +#include +#include +#include + +#include +#include +#include +#include "SysInfo.h" + +#ifdef Q_OS_LINUX +#include +#include "MangoHud.h" +#include "gamemode_client.h" +#endif + +#if defined(Q_OS_LINUX) +#include +#endif + +#if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#include +#include +#endif + +#if defined(Q_OS_MAC) +#if defined(SPARKLE_ENABLED) +#include "updater/MacSparkleUpdater.h" +#endif +#else +#include "updater/ProjTExternalUpdater.h" +#endif + +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include "console/WindowsConsole.hpp" +#endif + +#include "console/Console.hpp" + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + +static const QLatin1String liveCheckFile("live.check"); + +PixmapCache* PixmapCache::s_instance = nullptr; + +static bool isANSIColorConsole; + +static QString defaultLogFormat = QStringLiteral("%{time process}" + " " + "%{if-debug}Debug:%{endif}" + "%{if-info}Info:%{endif}" + "%{if-warning}Warning:%{endif}" + "%{if-critical}Critical:%{endif}" + "%{if-fatal}Fatal:%{endif}" + " " + "%{if-category}[%{category}] %{endif}" + "%{message}" + " " + "(%{function}:%{line})"); + +#define ansi_reset "\x1b[0m" +#define ansi_bold "\x1b[1m" +#define ansi_reset_bold "\x1b[22m" +#define ansi_faint "\x1b[2m" +#define ansi_italic "\x1b[3m" +#define ansi_red_fg "\x1b[31m" +#define ansi_green_fg "\x1b[32m" +#define ansi_yellow_fg "\x1b[33m" +#define ansi_blue_fg "\x1b[34m" +#define ansi_purple_fg "\x1b[35m" +#define ansi_inverse "\x1b[7m" + +// clang-format off +static QString ansiLogFormat = QStringLiteral( + ansi_faint "%{time process}" ansi_reset + " " + "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}" + "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}" + "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}" + "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}" + "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}" + " " + "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}" + "%{message}" + " " + ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset +); +// clang-format on + +#undef ansi_inverse +#undef ansi_purple_fg +#undef ansi_blue_fg +#undef ansi_yellow_fg +#undef ansi_green_fg +#undef ansi_red_fg +#undef ansi_italic +#undef ansi_faint +#undef ansi_bold +#undef ansi_reset_bold +#undef ansi_reset + +/** This is used so that we can output to the log file in addition to the CLI. */ +void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ + static std::mutex loggerMutex; + const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe + + if (isANSIColorConsole) + { + // ensure default is set for log file + qSetMessagePattern(defaultLogFormat); + } + + QString out = qFormatLogMessage(type, context, msg); + if (APPLICATION->logModel) + { + APPLICATION->logModel->append(MessageLevel::getLevel(type), out); + } + + out += QChar::LineFeed; + APPLICATION->logFile->write(out.toUtf8()); + APPLICATION->logFile->flush(); + + if (isANSIColorConsole) + { + // format ansi for console; + qSetMessagePattern(ansiLogFormat); + out = qFormatLogMessage(type, context, msg); + out += QChar::LineFeed; + } + + QTextStream(stderr) << out.toLocal8Bit(); + fflush(stderr); +} + +std::tuple read_lock_File(const QString& path) +{ + auto contents = QString(FS::read(path)); + auto lines = contents.split('\n'); + + QDateTime timestamp; + QString from, to, target, data_path; + for (auto line : lines) + { + auto index = line.indexOf("="); + if (index < 0) + continue; + auto left = line.left(index); + auto right = line.mid(index + 1); + if (left.toLower() == "timestamp") + { + timestamp = QDateTime::fromString(right, Qt::ISODate); + } + else if (left.toLower() == "from") + { + from = right; + } + else if (left.toLower() == "to") + { + to = right; + } + else if (left.toLower() == "target") + { + target = right; + } + else if (left.toLower() == "data_path") + { + data_path = right; + } + } + return std::make_tuple(timestamp, from, to, target, data_path); +} + +Application::Application(int& argc, char** argv) : QApplication(argc, argv) +{ +#if defined Q_OS_WIN32 + // attach the parent console if stdout not already captured + if (projt::console::AttachWindowsConsole()) + { + consoleAttached = true; + if (auto err = projt::console::EnableAnsiSupport(); !err) + { + isANSIColorConsole = true; + } + else + { + std::cout << "Error setting up ansi console" << err.message() << std::endl; + } + } +#else + if (projt::console::isConsole()) + { + isANSIColorConsole = true; + } +#endif + + setOrganizationName(BuildConfig.LAUNCHER_NAME); + setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); + setApplicationName(BuildConfig.LAUNCHER_NAME); + setApplicationDisplayName( + QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); + setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); + setDesktopFileName(BuildConfig.LAUNCHER_APPID); + m_startTime = QDateTime::currentDateTime(); + + // Don't quit on hiding the last window + this->setQuitOnLastWindowClosed(false); + this->setQuitLockEnabled(false); + + // Commandline parsing + QCommandLineParser parser; + parser.setApplicationDescription(BuildConfig.LAUNCHER_DISPLAYNAME); + + parser.addOptions( + { { { "d", "dir" }, "Use a custom path as application root (use '.' for current directory)", "directory" }, + { { "l", "launch" }, "Launch the specified instance (by instance ID)", "instance" }, + { { "s", "server" }, + "Join the specified server on launch (only valid in combination with --launch)", + "address" }, + { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" }, + { { "a", "profile" }, + "Use the account specified by its profile name (only valid in combination with --launch)", + "profile" }, + { { "o", "offline" }, + "Launch offline, with given player name (only valid in combination with --launch)", + "offline" }, + { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, + { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, + { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); + // Has to be positional for some OS to handle that properly + parser.addPositionalArgument("URL", + "Import the resource(s) at the given URL(s) (same as -I / --import)", + "[URL...]"); + + parser.addHelpOption(); + parser.addVersionOption(); + + parser.process(arguments()); + + m_instanceIdToLaunch = parser.value("launch"); + m_serverToJoin = parser.value("server"); + m_worldToJoin = parser.value("world"); + m_profileToUse = parser.value("profile"); + if (parser.isSet("offline")) + { + m_offline = true; + m_offlineName = parser.value("offline"); + } + m_liveCheck = parser.isSet("alive"); + + m_instanceIdToShowWindowOf = parser.value("show"); + + for (auto url : parser.values("import")) + { + m_urlsToImport.append(normalizeImportUrl(url)); + } + + // treat unspecified positional arguments as import urls + for (auto url : parser.positionalArguments()) + { + m_urlsToImport.append(normalizeImportUrl(url)); + } + + // error if --launch is missing with --server or --profile + if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_offline) + && m_instanceIdToLaunch.isEmpty()) + { + std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; + m_status = Application::Failed; + return; + } + + QString origcwdPath = QDir::currentPath(); + QString binPath = applicationDirPath(); + + { + // Root path is used for updates and portable data +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr + m_rootPath = foo.absolutePath(); +#elif defined(Q_OS_WIN32) + m_rootPath = binPath; +#elif defined(Q_OS_MAC) + QDir foo(FS::PathCombine(binPath, "../..")); + m_rootPath = foo.absolutePath(); + // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) + FS::updateTimestamp(m_rootPath); +#endif + } + + QString adjustedBy; + QString dataPath; + // change folder + QString dataDirEnv; + QString dirParam = parser.value("dir"); + if (!dirParam.isEmpty()) + { + // the dir param. it makes multimc data path point to whatever the user specified + // on command line + adjustedBy = "Command line"; + dataPath = dirParam; + } + else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value( + QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); + !dataDirEnv.isEmpty()) + { + adjustedBy = "System environment"; + dataPath = dataDirEnv; + } + else + { + QDir foo; + if (DesktopServices::isSnap()) + { + foo = QDir(qEnvironmentVariable("SNAP_USER_COMMON")); + } + else + { + foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); + } + + dataPath = foo.absolutePath(); + adjustedBy = "Persistent data path"; + +#ifndef Q_OS_MACOS + if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) + { + dataPath = portableUserData; + adjustedBy = "Portable user data path"; + m_portable = true; + } + else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) + { + dataPath = m_rootPath; + adjustedBy = "Portable data path"; + m_portable = true; + } +#endif + } + + if (!FS::ensureFolderPathExists(dataPath)) + { + showFatalErrorMessage("The launcher data folder could not be created.", + QString("The launcher data folder could not be created.\n" + "\n" + "Make sure you have the right permissions to the launcher data folder and any " + "folder needed to access it.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem.") + .arg(dataPath)); + return; + } + if (!QDir::setCurrent(dataPath)) + { + showFatalErrorMessage("The launcher data folder could not be opened.", + QString("The launcher data folder could not be opened.\n" + "\n" + "Make sure you have the right permissions to the launcher data folder.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem.") + .arg(dataPath)); + return; + } + m_dataPath = dataPath; + + /* + * Establish the mechanism for communication with an already running ProjTLauncher that uses the same binary. + * If there is one, tell it what the user actually wanted to do and exit. + * We want to initialize this before logging to avoid messing with the log of a potential already running copy. + * + * Using binary path (applicationDirPath) instead of data path prevents update conflicts + * when multiple instances with different data directories share the same binary. + */ + auto appID = + ApplicationId::fromPathAndVersion(QCoreApplication::applicationDirPath(), BuildConfig.printableVersionString()); + { + m_peerInstance = new LocalPeer(this, appID); + connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); + if (m_peerInstance->isClient()) + { + bool sentMessage = false; + int timeout = 2000; + + if (m_instanceIdToLaunch.isEmpty()) + { + ApplicationMessage activate; + activate.command = "activate"; + sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout); + + if (!m_urlsToImport.isEmpty()) + { + for (auto url : m_urlsToImport) + { + ApplicationMessage import; + import.command = "import"; + import.args.insert("url", url.toString()); + sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout); + } + } + } + else + { + ApplicationMessage launch; + launch.command = "launch"; + launch.args["id"] = m_instanceIdToLaunch; + + if (!m_serverToJoin.isEmpty()) + { + launch.args["server"] = m_serverToJoin; + } + else if (!m_worldToJoin.isEmpty()) + { + launch.args["world"] = m_worldToJoin; + } + if (!m_profileToUse.isEmpty()) + { + launch.args["profile"] = m_profileToUse; + } + if (m_offline) + { + launch.args["offline_enabled"] = "true"; + launch.args["offline_name"] = m_offlineName; + } + sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout); + } + if (sentMessage) + { + m_status = Application::Succeeded; + return; + } + else + { + std::cerr << "Unable to redirect command to already running instance\n"; + // C function not Qt function - event loop not started yet + ::exit(1); + } + } + } + + // init the logger + { + static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; + static const QString logBase = FS::PathCombine("logs", baseLogFile); + if (FS::ensureFolderPathExists("logs")) + { // if this did not fail + for (auto i = 0; i <= 4; i++) + if (auto oldName = baseLogFile.arg(i); QFile::exists(oldName)) // do not pointlessly delete new files if + // the old ones are not there + FS::move(oldName, logBase.arg(i)); + } + + for (auto i = 4; i > 0; i--) + { + const QString source = logBase.arg(i - 1); + if (QFile::exists(source)) + { + FS::move(source, logBase.arg(i)); + } + } + + logFile = std::unique_ptr(new QFile(logBase.arg(0))); + if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) + { + showFatalErrorMessage("The launcher data folder is not writable!", + QString("The launcher couldn't create a log file - the data folder is not writable.\n" + "\n" + "Make sure you have write permissions to the data folder.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem.") + .arg(dataPath)); + return; + } + qInstallMessageHandler(appDebugOutput); + qSetMessagePattern(defaultLogFormat); + + logModel.reset(new projt::launch::LaunchLogModel(this)); + + bool foundLoggingRules = false; + + auto logRulesFile = QStringLiteral("qtlogging.ini"); + auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); + + qInfo() << "Testing" << logRulesPath << "..."; + foundLoggingRules = QFile::exists(logRulesPath); + + // search the dataPath() + // seach app data standard path + if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) + { + logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); + if (!logRulesPath.isEmpty()) + { + qInfo() << "Found" << logRulesPath << "..."; + foundLoggingRules = true; + } + } + // seach root path + if (!foundLoggingRules) + { +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile); +#else + logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); +#endif + qInfo() << "Testing" << logRulesPath << "..."; + foundLoggingRules = QFile::exists(logRulesPath); + } + + if (foundLoggingRules) + { + // load and set logging rules + qInfo() << "Loading logging rules from:" << logRulesPath; + QSettings loggingRules(logRulesPath, QSettings::IniFormat); + loggingRules.beginGroup("Rules"); + QStringList rule_names = loggingRules.childKeys(); + QStringList rules; + qInfo() << "Setting log rules:"; + for (auto rule_name : rule_names) + { + auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); + rules.append(rule); + qInfo() << " " << rule; + } + auto rules_str = rules.join("\n"); + QLoggingCategory::setFilterRules(rules_str); + } + + qInfo() << "<> Log initialized."; + } + + { + bool migrated = false; + + if (!migrated) + migrated = handleDataMigration( + dataPath, + FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), + "PolyMC", + "polymc.cfg"); + if (!migrated) + migrated = handleDataMigration( + dataPath, + FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), + "MultiMC", + "multimc.cfg"); + } + + { + qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qInfo() << "Version : " << BuildConfig.printableVersionString(); + qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM; + qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT; + qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qInfo() << "Compiled for : " << BuildConfig.systemID(); + qInfo() << "Compiled by : " << BuildConfig.compilerID(); + qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); + if (adjustedBy.size()) + { + qInfo() << "Work dir before adjustment : " << origcwdPath; + qInfo() << "Work dir after adjustment : " << QDir::currentPath(); + qInfo() << "Adjusted by : " << adjustedBy; + } + else + { + qInfo() << "Work dir : " << QDir::currentPath(); + } + qInfo() << "Binary path : " << binPath; + qInfo() << "Application root path : " << m_rootPath; + if (!m_instanceIdToLaunch.isEmpty()) + { + qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch; + } + if (!m_serverToJoin.isEmpty()) + { + qInfo() << "Address of server to join :" << m_serverToJoin; + } + else if (!m_worldToJoin.isEmpty()) + { + qInfo() << "Name of the world to join :" << m_worldToJoin; + } + qInfo() << "<> Paths set."; + } + + if (m_liveCheck) + { + QFile check(liveCheckFile); + if (check.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + auto payload = appID.toString().toUtf8(); + if (check.write(payload) == payload.size()) + { + check.close(); + } + else + { + qWarning() << "Could not write into" << liveCheckFile << "!"; + check.remove(); // also closes file! + } + } + else + { + qWarning() << "Could not open" << liveCheckFile << "for writing!"; + } + } + + // Initialize application settings + { + // Provide a fallback for migration from PolyMC + m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this)); + + // Theming + m_settings->registerSetting("IconTheme", QString()); + m_settings->registerSetting("ApplicationTheme", QString()); + m_settings->registerSetting("BackgroundCat", QString("kitteh")); + m_settings->registerSetting("HubSearchEngine", QString()); + + // Remembered state + m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); + + m_settings->registerSetting("MenuBarInsteadOfToolBar", false); + + m_settings->registerSetting("NumberOfConcurrentTasks", 10); + m_settings->registerSetting("NumberOfConcurrentDownloads", 6); + m_settings->registerSetting("NumberOfManualRetries", 1); + m_settings->registerSetting("RequestTimeout", 60); + + QString defaultMonospace; + int defaultSize = 11; +#ifdef Q_OS_WIN32 + defaultMonospace = "Courier"; + defaultSize = 10; +#elif defined(Q_OS_MAC) + defaultMonospace = "Menlo"; +#else + defaultMonospace = "Monospace"; +#endif + + // resolve the font so the default actually matches + QFont consoleFont; + consoleFont.setFamily(defaultMonospace); + consoleFont.setStyleHint(QFont::Monospace); + consoleFont.setFixedPitch(true); + QFontInfo consoleFontInfo(consoleFont); + QString resolvedDefaultMonospace = consoleFontInfo.family(); + QFont resolvedFont(resolvedDefaultMonospace); + qDebug() << "Detected default console font:" << resolvedDefaultMonospace + << ", substitutions:" << resolvedFont.substitutions().join(','); + + m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace); + m_settings->registerSetting("ConsoleFontSize", defaultSize); + m_settings->registerSetting("ConsoleMaxLines", 100000); + m_settings->registerSetting("ConsoleOverflowStop", true); + + logModel->setMaxLines(getConsoleMaxLines(settings())); + logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings())); + logModel->setOverflowMessage( + tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); + + // Folders + m_settings->registerSetting("InstanceDir", "instances"); + m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods"); + m_settings->registerSetting("IconsDir", "icons"); + m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + m_settings->registerSetting("DownloadsDirWatchRecursive", false); + m_settings->registerSetting("MoveModsFromDownloadsDir", false); + m_settings->registerSetting("SkinsDir", "skins"); + m_settings->registerSetting("JavaDir", "java"); + + // Editors + m_settings->registerSetting("JsonEditor", QString()); + + // Language + m_settings->registerSetting("Language", QString()); + m_settings->registerSetting("UseSystemLocale", false); + + // Console + m_settings->registerSetting("ShowConsole", false); + m_settings->registerSetting("AutoCloseConsole", false); + m_settings->registerSetting("ShowConsoleOnError", true); + m_settings->registerSetting("LogPrePostOutput", true); + + // Window Size + m_settings->registerSetting({ "LaunchMaximized", "MCWindowMaximize" }, false); + m_settings->registerSetting({ "MinecraftWinWidth", "MCWindowWidth" }, 854); + m_settings->registerSetting({ "MinecraftWinHeight", "MCWindowHeight" }, 480); + + // Proxy Settings + m_settings->registerSetting("ProxyType", "None"); + m_settings->registerSetting({ "ProxyAddr", "ProxyHostName" }, "127.0.0.1"); + m_settings->registerSetting("ProxyPort", 8080); + m_settings->registerSetting({ "ProxyUser", "ProxyUsername" }, ""); + m_settings->registerSetting({ "ProxyPass", "ProxyPassword" }, ""); + + // Memory + m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); + m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem()); + m_settings->registerSetting("PermGen", 128); + // Java Settings + m_settings->registerSetting("JavaPath", ""); + m_settings->registerSetting("JavaSignature", ""); + m_settings->registerSetting("JavaArchitecture", ""); + m_settings->registerSetting("JavaRealArchitecture", ""); + m_settings->registerSetting("JavaVersion", ""); + m_settings->registerSetting("JavaVendor", ""); + m_settings->registerSetting("LastHostname", ""); + m_settings->registerSetting("JvmArgs", ""); + m_settings->registerSetting("IgnoreJavaCompatibility", false); + m_settings->registerSetting("IgnoreJavaWizard", false); + auto defaultEnableAutoJava = m_settings->get("JavaPath").toString().isEmpty(); + m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava); + m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava); + m_settings->registerSetting("UserAskedAboutAutomaticJavaDownload", false); + + // Legacy settings + m_settings->registerSetting("OnlineFixes", false); + + // Native library workarounds + m_settings->registerSetting("UseNativeOpenAL", false); + m_settings->registerSetting("CustomOpenALPath", ""); + m_settings->registerSetting("UseNativeGLFW", false); + m_settings->registerSetting("CustomGLFWPath", ""); + + // Performance related options + m_settings->registerSetting("EnableFeralGamemode", false); + m_settings->registerSetting("EnableMangoHud", false); + m_settings->registerSetting("UseDiscreteGpu", false); + m_settings->registerSetting("UseZink", false); + + // Game time + m_settings->registerSetting("ShowGameTime", true); + m_settings->registerSetting("ShowGlobalGameTime", true); + m_settings->registerSetting("RecordGameTime", true); + m_settings->registerSetting("ShowGameTimeWithoutDays", false); + + // Minecraft mods + m_settings->registerSetting("ModMetadataDisabled", false); + m_settings->registerSetting("ModDependenciesDisabled", false); + m_settings->registerSetting("SkipModpackUpdatePrompt", false); + m_settings->registerSetting("ShowModIncompat", false); + + // Minecraft offline player name + m_settings->registerSetting("LastOfflinePlayerName", ""); + + // Backup settings + m_settings->registerSetting("AutoBackupBeforeLaunch", false); + + // Wrapper command for launch + m_settings->registerSetting("WrapperCommand", ""); + + // Custom Commands + m_settings->registerSetting({ "PreLaunchCommand", "PreLaunchCmd" }, ""); + m_settings->registerSetting({ "PostExitCommand", "PostExitCmd" }, ""); + + // The cat + m_settings->registerSetting("TheCat", false); + m_settings->registerSetting("CatOpacity", 100); + m_settings->registerSetting("CatFit", "fit"); + + m_settings->registerSetting("StatusBarVisible", true); + + m_settings->registerSetting("ToolbarsLocked", false); + + // Instance + m_settings->registerSetting("InstSortMode", "Name"); + m_settings->registerSetting("InstRenamingMode", "AskEverytime"); + m_settings->registerSetting("SelectedInstance", QString()); + + // Window state and geometry + m_settings->registerSetting("MainWindowState", ""); + m_settings->registerSetting("MainWindowGeometry", ""); + + m_settings->registerSetting("ConsoleWindowState", ""); + m_settings->registerSetting("ConsoleWindowGeometry", ""); + + m_settings->registerSetting("SettingsGeometry", ""); + + m_settings->registerSetting("PagedGeometry", ""); + + m_settings->registerSetting("NewInstanceGeometry", ""); + + m_settings->registerSetting("UpdateDialogGeometry", ""); + + m_settings->registerSetting("ModDownloadGeometry", ""); + m_settings->registerSetting("RPDownloadGeometry", ""); + m_settings->registerSetting("TPDownloadGeometry", ""); + m_settings->registerSetting("ShaderDownloadGeometry", ""); + m_settings->registerSetting("DataPackDownloadGeometry", ""); + + m_settings->registerSetting("NewsGeometry", ""); + + // data pack window + // in future, more pages may be added - so this name is chosen to avoid needing migration + m_settings->registerSetting("WorldManagementGeometry", ""); + + // Pastebin settings with automatic migration from legacy format + migratePastebinSettings(); + + { + // Meta URL + m_settings->registerSetting("MetaURLOverride", ""); + + QUrl metaUrl(m_settings->get("MetaURLOverride").toString()); + + // get rid of invalid meta urls + if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https")) + m_settings->reset("MetaURLOverride"); + + // Resource URL + m_settings->registerSetting("ResourceURL", ""); + QString resourceUrlStr = m_settings->get("ResourceURL").toString(); + if (!resourceUrlStr.isEmpty()) + { + QUrl resourceUrl(resourceUrlStr); + if (!resourceUrl.isValid() || (resourceUrl.scheme() != "http" && resourceUrl.scheme() != "https")) + m_settings->reset("ResourceURL"); + } + } + m_settings->registerSetting("CloseAfterLaunch", false); + m_settings->registerSetting("QuitAfterGameStop", false); + + m_settings->registerSetting("Env", "{}"); + + // Custom Microsoft Authentication Client ID + m_settings->registerSetting("MSAClientIDOverride", ""); + + // Custom Flame API Key + { + m_settings->registerSetting("CFKeyOverride", ""); + m_settings->registerSetting("FlameKeyOverride", ""); + + QString flameKey = m_settings->get("CFKeyOverride").toString(); + + if (!flameKey.isEmpty()) + m_settings->set("FlameKeyOverride", flameKey); + m_settings->reset("CFKeyOverride"); + } + m_settings->registerSetting("ModrinthToken", ""); + m_settings->registerSetting("FallbackMRBlockedMods", true); + m_settings->registerSetting("UserAgentOverride", ""); + + // FTBApp instances + m_settings->registerSetting("FTBAppInstancesPath", ""); + + // Custom Technic Client ID + m_settings->registerSetting("TechnicClientID", ""); + + // Init page provider + { + m_globalSettingsProvider = std::make_shared(tr("Settings")); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + } + + PixmapCache::setInstance(new PixmapCache(this)); + + qInfo() << "<> Settings loaded."; + } + +#ifndef QT_NO_ACCESSIBILITY + QAccessible::installFactory(groupViewAccessibleFactory); +#endif /* !QT_NO_ACCESSIBILITY */ + + // initialize network access and proxy setup + { + m_network.reset(new QNetworkAccessManager()); + QString proxyTypeStr = settings()->get("ProxyType").toString(); + QString addr = settings()->get("ProxyAddr").toString(); + int port = settings()->get("ProxyPort").value(); + QString user = settings()->get("ProxyUser").toString(); + QString pass = settings()->get("ProxyPass").toString(); + updateProxySettings(proxyTypeStr, addr, port, user, pass); + qInfo() << "<> Network done."; + } + + // load translations + { + m_translations.reset(new TranslationsModel("translations")); + auto bcp47Name = m_settings->get("Language").toString(); + m_translations->selectLanguage(bcp47Name); + qInfo() << "Your language is" << bcp47Name; + qInfo() << "<> Translations loaded."; + } + + // Instance icons + { + auto setting = APPLICATION->settings()->getSetting("IconsDir"); + QStringList instFolders = { ":/icons/multimc/32x32/instances/", + ":/icons/multimc/50x50/instances/", + ":/icons/multimc/128x128/instances/", + ":/icons/multimc/scalable/instances/" }; + m_icons.reset(new projt::icons::IconList(instFolders, setting->get().toString())); + connect(setting.get(), + &Setting::SettingChanged, + [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); + qInfo() << "<> Instance icons initialized."; + } + + // Themes + m_themeManager = std::make_unique(); + + // initialize and load all instances + { + auto InstDirSetting = m_settings->getSetting("InstanceDir"); + // instance path: check for problems with '!' in instance path and warn the user in the log + // and remember that we have to show him a dialog when the gui starts (if it does so) + QString instDir = InstDirSetting->get().toString(); + qInfo() << "Instance path : " << instDir; + if (FS::checkProblemticPathJava(QDir(instDir))) + { + qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; + } + m_instances.reset(new InstanceList(m_settings, instDir, this)); + connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); + qInfo() << "Loading Instances..."; + m_instances->loadList(); + qInfo() << "<> Instances loaded."; + } + + // and accounts + { + m_accounts.reset(new AccountList(this)); + qInfo() << "Loading accounts..."; + m_accounts->setListFilePath("accounts.json", true); + m_accounts->loadList(); + m_accounts->fillQueue(); + qInfo() << "<> Accounts loaded."; + } + + // init the http meta cache + { + m_metacache.reset(new HttpMetaCache("metacache")); + m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath()); + m_metacache->addBase("libraries", QDir("libraries").absolutePath()); + m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); + m_metacache->addBase("general", QDir("cache").absolutePath()); + m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath()); + m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); + m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); + m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); + m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath()); + m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); + m_metacache->addBase("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath()); + m_metacache->addBase("translations", QDir("translations").absolutePath()); + m_metacache->addBase("meta", QDir("meta").absolutePath()); + m_metacache->addBase("java", QDir("cache/java").absolutePath()); + m_metacache->Load(); + qInfo() << "<> Cache initialized."; + } + + // now we have network, download translation updates + m_translations->downloadIndex(); + + // Register Java profiler integrations + m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); + m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); + m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory())); + for (auto profiler : m_profilers.values()) + { + profiler->registerSettings(m_settings); + } + + // Create the MCEdit thing... why is this here? + { + m_mcedit.reset(new MCEditTool(m_settings)); + } + +#ifdef Q_OS_MACOS + connect(this, &Application::clickedOnDock, [this]() { this->showMainWindow(); }); +#endif + + connect(this, + &Application::aboutToQuit, + [this]() + { + if (m_instances) + { + // save any remaining instance state + m_instances->saveNow(); + } + if (logFile) + { + logFile->flush(); + logFile->close(); + } + }); + + updateCapabilities(); + + detectLibraries(); + + // check update locks + { + auto update_log_path = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); + + auto update_lock = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.lock")); + if (update_lock.exists()) + { + auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock.absoluteFilePath()); + auto infoMsg = tr("This installation has a update lock file present at: %1\n" + "\n" + "Timestamp: %2\n" + "Updating from version %3 to %4\n" + "Target install path: %5\n" + "Data Path: %6" + "\n" + "This likely means that a update attempt failed. Please ensure your installation is in " + "working order before " + "proceeding.\n" + "Check the Prism Launcher updater log at: \n" + "%7\n" + "for details on the last update attempt.\n" + "\n" + "To delete this lock and proceed select \"Ignore\" below.") + .arg(update_lock.absoluteFilePath()) + .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) + .arg(update_log_path); + auto msgBox = QMessageBox(QMessageBox::Warning, + tr("Update In Progress"), + infoMsg, + QMessageBox::Ignore | QMessageBox::Abort); + msgBox.setDefaultButton(QMessageBox::Abort); + msgBox.setModal(true); + msgBox.setDetailedText(FS::read(update_log_path)); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + auto res = msgBox.exec(); + switch (res) + { + case QMessageBox::Ignore: + { + FS::deletePath(update_lock.absoluteFilePath()); + break; + } + case QMessageBox::Abort: [[fallthrough]]; + default: + { + qDebug() << "Exiting because update lockfile is present"; + QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); + return; + } + } + } + + auto update_fail_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.fail")); + if (update_fail_marker.exists()) + { + auto infoMsg = tr("An update attempt failed\n" + "\n" + "Please ensure your installation is in working order before " + "proceeding.\n" + "Check the Prism Launcher updater log at: \n" + "%1\n" + "for details on the last update attempt.") + .arg(update_log_path); + auto msgBox = QMessageBox(QMessageBox::Warning, + tr("Update Failed"), + infoMsg, + QMessageBox::Ignore | QMessageBox::Abort); + msgBox.setDefaultButton(QMessageBox::Abort); + msgBox.setModal(true); + msgBox.setDetailedText(FS::read(update_log_path)); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + auto res = msgBox.exec(); + switch (res) + { + case QMessageBox::Ignore: + { + FS::deletePath(update_fail_marker.absoluteFilePath()); + break; + } + case QMessageBox::Abort: [[fallthrough]]; + default: + { + qDebug() << "Exiting because update lockfile is present"; + QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); + return; + } + } + } + + auto update_success_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.success")); + if (update_success_marker.exists()) + { + auto infoMsg = tr("Update succeeded\n" + "\n" + "You are now running %1 .\n" + "Check the Prism Launcher updater log at: \n" + "%2\n" + "for details.") + .arg(BuildConfig.printableVersionString()) + .arg(update_log_path); + auto msgBox = new QMessageBox(QMessageBox::Information, tr("Update Succeeded"), infoMsg, QMessageBox::Ok); + msgBox->setDefaultButton(QMessageBox::Ok); + msgBox->setDetailedText(FS::read(update_log_path)); + msgBox->setAttribute(Qt::WA_DeleteOnClose); + msgBox->setMinimumWidth(460); + msgBox->adjustSize(); + msgBox->open(); + FS::deletePath(update_success_marker.absoluteFilePath()); + } + } + + // notify user if /tmp is mounted with `noexec` (#1693) + QString jvmArgs = m_settings->get("JvmArgs").toString(); + if (jvmArgs.indexOf("java.io.tmpdir") == -1) + { /* java.io.tmpdir is a valid workaround, so don't annoy */ + bool is_tmp_noexec = false; + +#if defined(Q_OS_LINUX) + + struct statvfs tmp_stat; + statvfs("/tmp", &tmp_stat); + is_tmp_noexec = tmp_stat.f_flag & ST_NOEXEC; + +#elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + + struct statfs tmp_stat; + statfs("/tmp", &tmp_stat); + is_tmp_noexec = tmp_stat.f_flags & MNT_NOEXEC; + +#endif + + if (is_tmp_noexec) + { + auto infoMsg = tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n" + "Some versions of Minecraft may not launch.\n" + "\n" + "You may solve this issue by remounting /tmp as 'exec' or setting " + "the java.io.tmpdir JVM argument to a writeable directory in a " + "filesystem where the 'exec' flag is set (e.g., /home/user/.local/tmp)\n"); + auto msgBox = new QMessageBox(QMessageBox::Information, + tr("Incompatible system configuration"), + infoMsg, + QMessageBox::Ok); + msgBox->setDefaultButton(QMessageBox::Ok); + msgBox->setAttribute(Qt::WA_DeleteOnClose); + msgBox->setMinimumWidth(460); + msgBox->adjustSize(); + msgBox->open(); + } + } + + if (createSetupWizard()) + { + return; + } + + m_themeManager->applyCurrentlySelectedTheme(true); + performMainStartupAction(); +} + +bool Application::createSetupWizard() +{ + bool javaRequired = [this]() + { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) + { + return false; + } + bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool(); + if (ignoreJavaWizard) + { + return false; + } + QString currentHostName = QHostInfo::localHostName(); + QString oldHostName = settings()->get("LastHostname").toString(); + if (currentHostName != oldHostName) + { + settings()->set("LastHostname", currentHostName); + return true; + } + QString currentJavaPath = settings()->get("JavaPath").toString(); + QString actualPath = FS::ResolveExecutable(currentJavaPath); + return actualPath.isNull(); + }(); + bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired + && !settings()->get("AutomaticJavaDownload").toBool() + && !settings()->get("AutomaticJavaSwitch").toBool() + && !settings()->get("UserAskedAboutAutomaticJavaDownload").toBool(); + bool languageRequired = settings()->get("Language").toString().isEmpty(); + bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; + bool searchEngineRequired = settings()->get("HubSearchEngine").toString().isEmpty(); + bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); + bool validIcons = m_themeManager->isValidIconTheme(settings()->get("IconTheme").toString()); + bool login = !m_accounts->anyAccountIsValid() && capabilities() & Application::SupportsMSA; + bool themeInterventionRequired = !validWidgets || !validIcons; + bool wizardRequired = + javaRequired || languageRequired || pasteInterventionRequired || searchEngineRequired || themeInterventionRequired + || askjava || login; + if (wizardRequired) + { + // set default theme after going into theme wizard + if (!validIcons) + settings()->set("IconTheme", QString("pe_colored")); + if (!validWidgets) + { +#if defined(Q_OS_WIN32) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + const QString style = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark + ? QStringLiteral("dark") + : QStringLiteral("bright"); +#else + const QString style = QStringLiteral("system"); +#endif + + settings()->set("ApplicationTheme", style); + } + + m_themeManager->applyCurrentlySelectedTheme(true); + + m_setupWizard = new SetupWizard(nullptr); + if (languageRequired) + { + m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); + } + + if (javaRequired) + { + m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); + } + else if (askjava) + { + m_setupWizard->addPage(new AutoJavaWizardPage(m_setupWizard)); + } + + if (pasteInterventionRequired) + { + m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); + } + + if (searchEngineRequired) + { + m_setupWizard->addPage(new SearchWizardPage(m_setupWizard)); + } + + if (themeInterventionRequired) + { + m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard)); + } + + if (login) + { + m_setupWizard->addPage(new LoginWizardPage(m_setupWizard)); + } + connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); + m_setupWizard->show(); + } + + return wizardRequired || login; +} + +bool Application::updaterEnabled() +{ +#if defined(Q_OS_MAC) + return BuildConfig.UPDATER_ENABLED; +#else + return BuildConfig.UPDATER_ENABLED && QFileInfo(FS::PathCombine(m_rootPath, updaterBinaryName())).isFile(); +#endif +} + +QString Application::updaterBinaryName() +{ + auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + exe_name.append(".exe"); +#else + exe_name.prepend("bin/"); +#endif + return exe_name; +} + +bool Application::event(QEvent* event) +{ +#ifdef Q_OS_MACOS + if (event->type() == QEvent::ApplicationStateChange) + { + auto ev = static_cast(event); + + if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive) + { + emit clickedOnDock(); + } + m_prevAppState = ev->applicationState(); + } +#endif + + if (event->type() == QEvent::FileOpen) + { + if (!m_mainWindow) + { + showMainWindow(false); + } + auto ev = static_cast(event); + m_mainWindow->processURLs({ ev->url() }); + } + + return QApplication::event(event); +} + +void Application::setupWizardFinished(int status) +{ + qDebug() << "Wizard result =" << status; + performMainStartupAction(); +} + +void Application::performMainStartupAction() +{ + m_status = Application::Initialized; + if (!m_instanceIdToLaunch.isEmpty()) + { + auto inst = instances()->getInstanceById(m_instanceIdToLaunch); + if (inst) + { + MinecraftTarget::Ptr targetToJoin = nullptr; + MinecraftAccountPtr accountToUse = nullptr; + + qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; + if (!m_serverToJoin.isEmpty()) + { + auto parsedTarget = MinecraftTarget::parse(m_serverToJoin, false); + if (!parsedTarget.isValid()) + { + qWarning() << "Invalid server address:" << m_serverToJoin; + } + else + { + targetToJoin.reset(new MinecraftTarget(parsedTarget)); + qDebug() << " Launching with server" << m_serverToJoin; + } + } + else if (!m_worldToJoin.isEmpty()) + { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_worldToJoin, true))); + qDebug() << " Launching with world" << m_worldToJoin; + } + + if (!m_profileToUse.isEmpty()) + { + accountToUse = accounts()->getAccountByProfileName(m_profileToUse); + if (!accountToUse) + { + return; + } + qDebug() << " Launching with account" << m_profileToUse; + } + + launch(inst, m_offline ? LaunchMode::Offline : LaunchMode::Normal, targetToJoin, accountToUse, m_offlineName); + return; + } + } + if (!m_instanceIdToShowWindowOf.isEmpty()) + { + auto inst = instances()->getInstanceById(m_instanceIdToShowWindowOf); + if (inst) + { + qDebug() << "<> Showing window of instance " << m_instanceIdToShowWindowOf; + showInstanceWindow(inst); + return; + } + } + if (!m_mainWindow) + { + // normal main window + showMainWindow(false); + qDebug() << "<> Main window shown."; + } + + // initialize the updater + if (updaterEnabled()) + { + qDebug() << "Initializing updater"; +#ifdef Q_OS_MAC +#if defined(SPARKLE_ENABLED) + m_updater.reset(new MacSparkleUpdater()); +#endif +#else + m_updater.reset(new ProjTExternalUpdater(m_mainWindow, m_rootPath, m_dataPath)); +#endif + qDebug() << "<> Updater started."; + } + + { // delete instances tmp dirctory + auto instDir = m_settings->get("InstanceDir").toString(); + const QString tempRoot = FS::PathCombine(instDir, ".tmp"); + FS::deletePath(tempRoot); + } + + if (!m_urlsToImport.isEmpty()) + { + qDebug() << "<> Importing from url:" << m_urlsToImport; + m_mainWindow->processURLs(m_urlsToImport); + } +} + +void Application::showFatalErrorMessage(const QString& title, const QString& content) +{ + m_status = Application::Failed; + auto dialog = CustomMessageBox::selectable(nullptr, title, content, QMessageBox::Critical); + dialog->exec(); +} + +Application::~Application() +{ + // Shut down logger by setting the logger function to nothing + qInstallMessageHandler(nullptr); + +#if defined Q_OS_WIN32 + // Detach from Windows console + if (consoleAttached) + { + fclose(stdout); + fclose(stdin); + fclose(stderr); + FreeConsole(); + } +#endif +} + +void Application::messageReceived(const QByteArray& message) +{ + ApplicationMessage received; + received.parse(message); + + auto& command = received.command; + + if (status() != Initialized) + { + bool isLoginAtempt = false; + if (command == "import") + { + QString url = received.args["url"]; + isLoginAtempt = !url.isEmpty() && normalizeImportUrl(url).scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME; + } + if (!isLoginAtempt) + { + qDebug() << "Received message" << message << "while still initializing. It will be ignored."; + return; + } + } + + if (command == "activate") + { + showMainWindow(); + } + else if (command == "import") + { + QString url = received.args["url"]; + if (url.isEmpty()) + { + qWarning() << "Received" << command << "message without a zip path/URL."; + return; + } + if (!m_mainWindow) + { + showMainWindow(false); + } + m_mainWindow->processURLs({ normalizeImportUrl(url) }); + } + else if (command == "launch") + { + QString id = received.args["id"]; + QString server = received.args["server"]; + QString world = received.args["world"]; + QString profile = received.args["profile"]; + bool offline = received.args["offline_enabled"] == "true"; + QString offlineName = received.args["offline_name"]; + + InstancePtr instance; + if (!id.isEmpty()) + { + instance = instances()->getInstanceById(id); + if (!instance) + { + qWarning() << "Launch command requires an valid instance ID. " << id << "resolves to nothing."; + return; + } + } + else + { + qWarning() << "Launch command called without an instance ID..."; + return; + } + + MinecraftTarget::Ptr serverObject = nullptr; + if (!server.isEmpty()) + { + serverObject = std::make_shared(MinecraftTarget::parse(server, false)); + } + else if (!world.isEmpty()) + { + serverObject = std::make_shared(MinecraftTarget::parse(world, true)); + } + MinecraftAccountPtr accountObject; + if (!profile.isEmpty()) + { + accountObject = accounts()->getAccountByProfileName(profile); + if (!accountObject) + { + qWarning() << "Launch command requires the specified profile to be valid. " << profile + << "does not resolve to any account."; + return; + } + } + + launch(instance, offline ? LaunchMode::Offline : LaunchMode::Normal, serverObject, accountObject, offlineName); + } + else + { + qWarning() << "Received invalid message" << message; + } +} + +std::shared_ptr Application::translations() +{ + return m_translations; +} + +std::shared_ptr Application::runtimeCatalog() +{ + if (!m_runtimeCatalog) + { + m_runtimeCatalog.reset(new projt::java::RuntimeCatalog()); + } + return m_runtimeCatalog; +} + +QIcon Application::logo() +{ + return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); +} + +bool Application::openJsonEditor(const QString& filename) +{ + const QString file = QDir::current().absoluteFilePath(filename); + if (m_settings->get("JsonEditor").toString().isEmpty()) + { + return DesktopServices::openUrl(QUrl::fromLocalFile(file)); + } + else + { + // return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file); + return DesktopServices::run(m_settings->get("JsonEditor").toString(), { file }); + } +} + +bool Application::launch(InstancePtr instance, + LaunchMode mode, + MinecraftTarget::Ptr targetToJoin, + MinecraftAccountPtr accountToUse, + const QString& offlineName) +{ + if (m_updateRunning) + { + qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; + } + else if (instance->canLaunch()) + { + // Auto-backup before launch if enabled + if (settings()->get("AutoBackupBeforeLaunch").toBool()) + { + qDebug() << "Creating auto-backup before launch..."; + + QProgressDialog* progress = + new QProgressDialog("Creating backup before launch...", QString(), 0, 0, m_mainWindow); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + progress->setValue(0); + progress->setCancelButton(nullptr); + progress->show(); + QApplication::processEvents(); + + BackupManager* backupManager = new BackupManager(this); + connect(backupManager, + &BackupManager::backupCreated, + this, + [this, instance, mode, offlineName, progress, backupManager](const QString& instanceId, + const QString& backupName) + { + if (instanceId == instance->id()) + { + qDebug() << "Auto-backup before launch completed."; + progress->close(); + progress->deleteLater(); + backupManager->deleteLater(); + continueLaunchAfterBackup(instanceId, mode, offlineName); + } + }); + connect(backupManager, + &BackupManager::backupFailed, + this, + [progress, backupManager](const QString&, const QString& error) + { + qWarning() << "Auto-backup before launch failed:" << error; + progress->close(); + progress->deleteLater(); + backupManager->deleteLater(); + }); + BackupOptions options; + options.includeSaves = true; + options.includeConfig = true; + options.includeOptions = true; + options.includeMods = false; + backupManager->createBackupAsync(instance, "auto-backup-pre-launch", options); + // launch işlemini backup tamamlanınca başlatıyoruz, burada return ile çıkıyoruz + return true; + } + continueLaunchAfterBackup(instance->id(), mode, offlineName); + return true; + } + return false; +} + +void Application::continueLaunchAfterBackup(QString instanceId, LaunchMode mode, QString offlineName) +{ + InstancePtr instance = instances()->getInstanceById(instanceId); + InstanceWindow* window = nullptr; + + if (instance->settings()->get("ShowConsole").toBool()) + { + window = showInstanceWindow(instance); + } + + // Get window from instance if not already set + if (!window) + { + window = instance->getInstanceWindow(); + } + if (window) + { + if (!window->saveAll()) + { + return; + } + } + + // Create and configure launch controller + auto controller = makeShared(); + controller->setInstance(instance); + controller->setLaunchMode(mode); + controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); + controller->setOfflineName(offlineName); + if (window) + { + controller->setParentWidget(window); + } + else if (m_mainWindow) + { + controller->setParentWidget(m_mainWindow); + } + connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded); + connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); + connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); }); + + // Store controller in instance + instance->setLaunchController(controller); + controller->executeTask(); +} + +bool Application::kill(InstancePtr instance) +{ + if (!instance->isRunning()) + { + qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; + return false; + } + + // Get controller from instance + auto controller = instance->getLaunchController(); + if (controller) + { + return controller->abort(); + } + return true; +} + +void Application::closeCurrentWindow() +{ + if (focusWindow()) + focusWindow()->close(); +} + +void Application::addRunningInstance() +{ + m_runningInstances++; + if (m_runningInstances == 1) + { + emit updateAllowedChanged(false); + } +} + +void Application::subRunningInstance() +{ + if (m_runningInstances == 0) + { + qCritical() << "Something went really wrong and we now have less than 0 running instances... WTF"; + return; + } + m_runningInstances--; + if (m_runningInstances == 0) + { + emit updateAllowedChanged(true); + } +} + +bool Application::shouldExitNow() const +{ + return m_runningInstances == 0 && m_openWindows == 0; +} + +bool Application::updatesAreAllowed() +{ + return m_runningInstances == 0; +} + +void Application::updateIsRunning(bool running) +{ + m_updateRunning = running; +} + +void Application::controllerSucceeded() +{ + auto controller = qobject_cast(sender()); + if (!controller) + return; + auto instance = controller->instance(); + + // on success, do... + if (instance->settings()->get("AutoCloseConsole").toBool()) + { + auto window = instance->getInstanceWindow(); + if (window) + { + QMetaObject::invokeMethod(window, &QWidget::close, Qt::QueuedConnection); + } + } + + // Clear controller from instance + instance->setLaunchController(nullptr); + subRunningInstance(); + + // quit when there are no more windows. + if (shouldExitNow()) + { + m_status = Status::Succeeded; + exit(0); + } +} + +void Application::controllerFailed(const QString& error) +{ + Q_UNUSED(error); + auto controller = qobject_cast(sender()); + if (!controller) + return; + auto instance = controller->instance(); + + // on failure, do... nothing + // Clear controller from instance + instance->setLaunchController(nullptr); + subRunningInstance(); + + // quit when there are no more windows. + if (shouldExitNow()) + { + m_status = Status::Failed; + exit(1); + } +} + +void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) +{ + if (!m_globalSettingsProvider) + { + return; + } + emit globalSettingsAboutToOpen(); + { + SettingsObject::Lock lock(APPLICATION->settings()); + PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent); + connect(&dlg, &PageDialog::applied, this, &Application::globalSettingsApplied); + dlg.exec(); + } +} + +MainWindow* Application::showMainWindow(bool minimized) +{ + if (m_mainWindow) + { + m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized); + m_mainWindow->raise(); + m_mainWindow->activateWindow(); + } + else + { + m_mainWindow = new MainWindow(); + m_mainWindow->restoreState( + QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toString().toUtf8())); + m_mainWindow->restoreGeometry( + QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toString().toUtf8())); + + if (minimized) + { + m_mainWindow->showMinimized(); + } + else + { + m_mainWindow->show(); + } + + m_mainWindow->checkInstancePathForProblems(); + connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged); + connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose); + m_openWindows++; + } + return m_mainWindow; +} + +ViewLogWindow* Application::showLogWindow() +{ + if (m_viewLogWindow) + { + m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized); + m_viewLogWindow->raise(); + m_viewLogWindow->activateWindow(); + } + else + { + m_viewLogWindow = new ViewLogWindow(); + connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose); + m_openWindows++; + } + return m_viewLogWindow; +} + +InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) +{ + if (!instance) + return nullptr; + + auto window = instance->getInstanceWindow(); + + if (window) + { +// If the window is minimized on macOS or Windows, activate and bring it up +#ifdef Q_OS_MACOS + if (window->isMinimized()) + { + window->setWindowState(window->windowState() & ~Qt::WindowMinimized); + } +#elif defined(Q_OS_WIN) + if (window->isMinimized()) + { + window->showNormal(); + } +#endif + + window->raise(); + window->activateWindow(); + } + else + { + window = new InstanceWindow(instance); + instance->setInstanceWindow(window); + m_openWindows++; + connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose); + } + + if (!page.isEmpty()) + { + window->selectPage(page); + } + + // Update controller parent if exists + auto controller = instance->getLaunchController(); + if (controller) + { + controller->setParentWidget(window); + } + return window; +} + +void Application::on_windowClose() +{ + m_openWindows--; + auto instWindow = qobject_cast(sender()); + if (instWindow) + { + // Get instance and clear window reference + auto instance = instances()->getInstanceById(instWindow->instanceId()); + if (instance) + { + instance->setInstanceWindow(nullptr); + + // Update controller parent if exists + auto controller = instance->getLaunchController(); + if (controller) + { + controller->setParentWidget(m_mainWindow); + } + } + } + auto mainWindow = qobject_cast(sender()); + if (mainWindow) + { + m_mainWindow = nullptr; + } + auto logWindow = qobject_cast(sender()); + if (logWindow) + { + m_viewLogWindow = nullptr; + } + // quit when there are no more windows. + if (shouldExitNow()) + { + exit(0); + } +} + +void Application::updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password) +{ + // Set the application proxy settings. + if (proxyTypeStr == "SOCKS5") + { + QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, addr, port, user, password)); + } + else if (proxyTypeStr == "HTTP") + { + QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, addr, port, user, password)); + } + else if (proxyTypeStr == "None") + { + // If we have no proxy set, set no proxy and return. + QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::NoProxy)); + } + else + { + // If we have "Default" selected, set Qt to use the system proxy settings. + QNetworkProxyFactory::setUseSystemConfiguration(true); + } + + qDebug() << "Detecting proxy settings..."; + QNetworkProxy proxy = QNetworkProxy::applicationProxy(); + m_network->setProxy(proxy); + + QString proxyDesc; + if (proxy.type() == QNetworkProxy::NoProxy) + { + qDebug() << "Using no proxy is an option!"; + return; + } + switch (proxy.type()) + { + case QNetworkProxy::DefaultProxy: proxyDesc = "Default proxy: "; break; + case QNetworkProxy::Socks5Proxy: proxyDesc = "Socks5 proxy: "; break; + case QNetworkProxy::HttpProxy: proxyDesc = "HTTP proxy: "; break; + case QNetworkProxy::HttpCachingProxy: proxyDesc = "HTTP caching: "; break; + case QNetworkProxy::FtpCachingProxy: proxyDesc = "FTP caching: "; break; + default: proxyDesc = "DERP proxy: "; break; + } + proxyDesc += QString("%1:%2").arg(proxy.hostName()).arg(proxy.port()); + qDebug() << proxyDesc; +} + +shared_qobject_ptr Application::metacache() +{ + return m_metacache; +} + +shared_qobject_ptr Application::network() +{ + return m_network; +} + +shared_qobject_ptr Application::metadataIndex() +{ + if (!m_metadataIndex) + { + m_metadataIndex.reset(new projt::meta::MetaIndex()); + } + return m_metadataIndex; +} + +void Application::updateCapabilities() +{ + m_capabilities = None; + if (!getMSAClientID().isEmpty()) + m_capabilities |= SupportsMSA; + if (!getFlameAPIKey().isEmpty()) + m_capabilities |= SupportsFlame; + +#ifdef Q_OS_LINUX + if (gamemode_query_status() >= 0) + m_capabilities |= SupportsGameMode; + + if (!MangoHud::getLibraryString().isEmpty()) + m_capabilities |= SupportsMangoHud; +#endif +} + +void Application::detectLibraries() +{ +#ifdef Q_OS_LINUX + m_detectedGLFWPath = MangoHud::findLibrary(BuildConfig.GLFW_LIBRARY_NAME); + m_detectedOpenALPath = MangoHud::findLibrary(BuildConfig.OPENAL_LIBRARY_NAME); + qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath; +#endif +} + +QString Application::getJarPath(QString jarFile) +{ + QStringList potentialPaths = { +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME), +#endif + FS::PathCombine(m_rootPath, "jars"), + FS::PathCombine(applicationDirPath(), "jars"), + FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging + }; + for (QString p : potentialPaths) + { + QString jarPath = FS::PathCombine(p, jarFile); + if (QFileInfo(jarPath).isFile()) + return jarPath; + } + return {}; +} + +QString Application::getMSAClientID() +{ + QString clientIDOverride = m_settings->get("MSAClientIDOverride").toString(); + if (!clientIDOverride.isEmpty()) + { + return clientIDOverride; + } + + return BuildConfig.MSA_CLIENT_ID; +} + +QString Application::getFlameAPIKey() +{ + QString keyOverride = m_settings->get("FlameKeyOverride").toString(); + if (!keyOverride.isEmpty()) + { + return keyOverride; + } + + return BuildConfig.FLAME_API_KEY; +} + +QString Application::getModrinthAPIToken() +{ + QString tokenOverride = m_settings->get("ModrinthToken").toString(); + if (!tokenOverride.isEmpty()) + return tokenOverride; + + return QString(); +} + +QString Application::getUserAgent() +{ + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) + { + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); + } + + return BuildConfig.USER_AGENT; +} + +bool Application::handleDataMigration(const QString& currentData, + const QString& oldData, + const QString& name, + const QString& configFile) const +{ + QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt"); + QStringList configPaths = { FS::PathCombine(oldData, configFile), + FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) }; + + QLocale locale; + + // Is there a valid config at the old location? + bool configExists = false; + for (QString configPath : configPaths) + { + configExists |= QFileInfo::exists(configPath); + } + + if (!configExists || QFileInfo::exists(nomigratePath)) + { + qDebug() << "<> No migration needed from" << name; + return false; + } + + QString message; + bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE)); + + if (currentExists) + { + message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to " + "migrate yourself. Do " + "you want to be reminded of the pending data migration next time you start %2?") + .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); + } + else + { + message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?") + .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); + + QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log")); + if (logInfo.exists()) + { + QString lastModified = logInfo.lastModified().toString(locale.dateFormat()); + message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location " + "of %3?") + .arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME); + } + } + + QMessageBox::StandardButton askMoveDialogue = QMessageBox::question(nullptr, + BuildConfig.LAUNCHER_DISPLAYNAME, + message, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes); + + auto setDoNotMigrate = [&nomigratePath] + { + QFile file(nomigratePath); + if (!file.open(QIODevice::WriteOnly)) + { + qWarning() << "setDoNotMigrate failed; Failed to open file '" << file.fileName() << "' for writing!"; + } + }; + + // create no-migrate file if user doesn't want to migrate + if (askMoveDialogue != QMessageBox::Yes) + { + qDebug() << "<> Migration declined for" << name; + setDoNotMigrate(); + return currentExists; // cancel further migrations, if we already have a data directory + } + + if (!currentExists) + { + // Migrate! + using namespace Filters; + + QList filters; + filters.append(equals(configFile)); + filters.append(equals(BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory + // before + filters.append(startsWith("logs/")); + filters.append(equals("accounts.json")); + filters.append(startsWith("accounts/")); + filters.append(startsWith("assets/")); + filters.append(startsWith("icons/")); + filters.append(startsWith("instances/")); + filters.append(startsWith("libraries/")); + filters.append(startsWith("mods/")); + filters.append(startsWith("themes/")); + + ProgressDialog diag; + DataMigrationTask task(oldData, currentData, any(std::move(filters))); + if (diag.execWithTask(task)) + { + qDebug() << "<> Migration succeeded"; + setDoNotMigrate(); + } + else + { + QString reason = task.failReason(); + QMessageBox::critical(nullptr, + BuildConfig.LAUNCHER_DISPLAYNAME, + tr("Migration failed! Reason: %1").arg(reason)); + } + } + else + { + qWarning() << "<> Migration was skipped, due to existing data"; + } + return true; +} + +void Application::triggerUpdateCheck() +{ + if (m_updater) + { + qDebug() << "Checking for updates."; + m_updater->setBetaAllowed(false); // There are no other channels than stable + m_updater->checkForUpdates(); + } + else + { + qDebug() << "Updater not available."; + } +} + +QUrl Application::normalizeImportUrl(QString const& url) +{ + auto local_file = QFileInfo(url); + if (local_file.exists()) + { + return QUrl::fromLocalFile(local_file.absoluteFilePath()); + } + else + { + return QUrl::fromUserInput(url); + } +} + +const QString Application::javaPath() +{ + return m_settings->get("JavaDir").toString(); +} + +void Application::addQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1; +} + +void Application::removeQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + auto count = m_qsaveResources.value(path, 0) - 1; + if (count <= 0) + { + m_qsaveResources.remove(path); + } + else + { + m_qsaveResources[path] = count; + } +} + +bool Application::checkQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + for (auto partialPath : m_qsaveResources.keys()) + { + if (path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0) + { + return true; + } + } + return false; +} + +void Application::migratePastebinSettings() +{ + m_settings->registerSetting("PastebinURL", ""); + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); + m_settings->registerSetting("PastebinCustomAPIBase", ""); + m_settings->registerSetting("PastebinMigrationDone", false); + + // Skip if migration already completed + if (m_settings->get("PastebinMigrationDone").toBool()) + { + return; + } + + // Check if legacy URL exists + QString pastebinURL = m_settings->get("PastebinURL").toString(); + if (!pastebinURL.isEmpty()) + { + // Migrate from legacy 0x0.st URL to new format + bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; + if (!userHadDefaultPastebin) + { + m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); + m_settings->set("PastebinCustomAPIBase", pastebinURL); + } + m_settings->reset("PastebinURL"); + } + + // Validate PastebinType + bool ok; + int pasteType = m_settings->get("PastebinType").toInt(&ok); + if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) + { + m_settings->reset("PastebinType"); + m_settings->reset("PastebinCustomAPIBase"); + } + + // Mark migration as complete + m_settings->set("PastebinMigrationDone", true); +} diff --git a/archived/projt-launcher/launcher/Application.h b/archived/projt-launcher/launcher/Application.h new file mode 100644 index 0000000000..9367586361 --- /dev/null +++ b/archived/projt-launcher/launcher/Application.h @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Tayou + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "LaunchMode.h" +#include "launch/LaunchLogModel.hpp" +#include "minecraft/launch/MinecraftTarget.hpp" + +class LaunchController; +class LocalPeer; +class InstanceWindow; +class MainWindow; +class ViewLogWindow; +class SetupWizard; +class GenericPageProvider; +class QFile; +class HttpMetaCache; +class SettingsObject; +class InstanceList; +class AccountList; +namespace projt::icons +{ + class IconList; +} +class QNetworkAccessManager; +namespace projt::java +{ + class RuntimeCatalog; +} +class ExternalUpdater; +class BaseProfilerFactory; +class BaseDetachedToolFactory; +class TranslationsModel; +class Theme; +class MCEditTool; +class ThemeManager; +class IconTheme; + +namespace projt::meta +{ + class MetaIndex; +} + +#if defined(APPLICATION) +#undef APPLICATION +#endif +#define APPLICATION (static_cast(QCoreApplication::instance())) + +// Used for checking if is a test +#if defined(APPLICATION_DYN) +#undef APPLICATION_DYN +#endif +#define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) + +class Application : public QApplication +{ + // friends for the purpose of limiting access to deprecated stuff + Q_OBJECT + public: + enum Status + { + StartingUp, + Failed, + Succeeded, + Initialized + }; + + enum Capability + { + None = 0, + + SupportsMSA = 1 << 0, + SupportsFlame = 1 << 1, + SupportsGameMode = 1 << 2, + SupportsMangoHud = 1 << 3, + }; + Q_DECLARE_FLAGS(Capabilities, Capability) + + public: + Application(int& argc, char** argv); + virtual ~Application(); + + bool event(QEvent* event) override; + + std::shared_ptr settings() const + { + return m_settings; + } + + qint64 timeSinceStart() const + { + return m_startTime.msecsTo(QDateTime::currentDateTime()); + } + + QIcon logo(); + + ThemeManager* themeManager() + { + return m_themeManager.get(); + } + + shared_qobject_ptr updater() + { + return m_updater; + } + + void triggerUpdateCheck(); + + std::shared_ptr translations(); + + std::shared_ptr runtimeCatalog(); + + std::shared_ptr instances() const + { + return m_instances; + } + + std::shared_ptr icons() const + { + return m_icons; + } + + MCEditTool* mcedit() const + { + return m_mcedit.get(); + } + + shared_qobject_ptr accounts() const + { + return m_accounts; + } + + Status status() const + { + return m_status; + } + + const QMap>& profilers() const + { + return m_profilers; + } + + void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); + + shared_qobject_ptr network(); + + shared_qobject_ptr metacache(); + + shared_qobject_ptr metadataIndex(); + + void updateCapabilities(); + + void detectLibraries(); + + /*! + * Finds and returns the full path to a jar file. + * Returns a null-string if it could not be found. + */ + QString getJarPath(QString jarFile); + + QString getMSAClientID(); + QString getFlameAPIKey(); + QString getModrinthAPIToken(); + QString getUserAgent(); + + /// this is the root of the 'installation'. Used for automatic updates + const QString& root() + { + return m_rootPath; + } + + /// the data path the application is using + const QString& dataRoot() + { + return m_dataPath; + } + + /// the java installed path the application is using + const QString javaPath(); + + bool isPortable() + { + return m_portable; + } + + const Capabilities capabilities() + { + return m_capabilities; + } + + /*! + * Opens a json file using either a system default editor, or, if not empty, the editor + * specified in the settings + */ + bool openJsonEditor(const QString& filename); + + InstanceWindow* showInstanceWindow(InstancePtr instance, QString page = QString()); + MainWindow* showMainWindow(bool minimized = false); + ViewLogWindow* showLogWindow(); + + void updateIsRunning(bool running); + bool updatesAreAllowed(); + + void ShowGlobalSettings(class QWidget* parent, QString open_page = QString()); + + bool updaterEnabled(); + QString updaterBinaryName(); + + QUrl normalizeImportUrl(QString const& url); + + signals: + void updateAllowedChanged(bool status); + void globalSettingsAboutToOpen(); + void globalSettingsApplied(); + int currentCatChanged(int index); + + void oauthReplyRecieved(QVariantMap); + +#ifdef Q_OS_MACOS + void clickedOnDock(); +#endif + + public slots: + bool launch(InstancePtr instance, + LaunchMode mode = LaunchMode::Normal, + MinecraftTarget::Ptr targetToJoin = nullptr, + MinecraftAccountPtr accountToUse = nullptr, + const QString& offlineName = QString()); + bool kill(InstancePtr instance); + void closeCurrentWindow(); + + private slots: + void on_windowClose(); + void messageReceived(const QByteArray& message); + void controllerSucceeded(); + void controllerFailed(const QString& error); + void setupWizardFinished(int status); + void continueLaunchAfterBackup(QString instanceId, LaunchMode mode, QString offlineName); + + private: + bool handleDataMigration(const QString& currentData, + const QString& oldData, + const QString& name, + const QString& configFile) const; + bool createSetupWizard(); + void performMainStartupAction(); + + // sets the fatal error message and m_status to Failed. + void showFatalErrorMessage(const QString& title, const QString& content); + + private: + void addRunningInstance(); + void subRunningInstance(); + bool shouldExitNow() const; + + /// Migrates legacy pastebin settings to new format + void migratePastebinSettings(); + + private: + QDateTime m_startTime; + + shared_qobject_ptr m_network; + + shared_qobject_ptr m_updater; + shared_qobject_ptr m_accounts; + + shared_qobject_ptr m_metacache; + shared_qobject_ptr m_metadataIndex; + + std::shared_ptr m_settings; + std::shared_ptr m_instances; + std::shared_ptr m_icons; + std::shared_ptr m_runtimeCatalog; + std::shared_ptr m_translations; + std::shared_ptr m_globalSettingsProvider; + std::unique_ptr m_mcedit; + QSet m_features; + std::unique_ptr m_themeManager; + + QMap> m_profilers; + + QString m_rootPath; + QString m_dataPath; + Status m_status = Application::StartingUp; + Capabilities m_capabilities; + bool m_portable = false; + +#ifdef Q_OS_MACOS + Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; +#endif + +#if defined Q_OS_WIN32 + // used on Windows to attach the standard IO streams + bool consoleAttached = false; +#endif + + // main state variables + size_t m_openWindows = 0; + size_t m_runningInstances = 0; + bool m_updateRunning = false; + + // main window, if any + MainWindow* m_mainWindow = nullptr; + + // log window, if any + ViewLogWindow* m_viewLogWindow = nullptr; + + // peer launcher instance connector - used to implement single instance launcher and signalling + LocalPeer* m_peerInstance = nullptr; + + SetupWizard* m_setupWizard = nullptr; + + public: + QString m_detectedGLFWPath; + QString m_detectedOpenALPath; + QString m_instanceIdToLaunch; + QString m_serverToJoin; + QString m_worldToJoin; + QString m_profileToUse; + bool m_offline = false; + QString m_offlineName; + bool m_liveCheck = false; + QList m_urlsToImport; + QString m_instanceIdToShowWindowOf; + std::unique_ptr logFile; + shared_qobject_ptr logModel; + + public: + void addQSavePath(QString); + void removeQSavePath(QString); + bool checkQSavePath(QString); + + private: + QHash m_qsaveResources; + mutable QMutex m_qsaveResourcesMutex; +}; diff --git a/archived/projt-launcher/launcher/ApplicationMessage.cpp b/archived/projt-launcher/launcher/ApplicationMessage.cpp new file mode 100644 index 0000000000..5dec252d96 --- /dev/null +++ b/archived/projt-launcher/launcher/ApplicationMessage.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ +#include "ApplicationMessage.h" + +#include +#include +#include "Json.h" + +void ApplicationMessage::parse(const QByteArray& input) +{ + auto doc = Json::requireDocument(input, "ApplicationMessage"); + auto root = Json::requireObject(doc, "ApplicationMessage"); + + command = root.value("command").toString(); + args.clear(); + + auto parsedArgs = root.value("args").toObject(); + for (auto iter = parsedArgs.constBegin(); iter != parsedArgs.constEnd(); iter++) + { + args.insert(iter.key(), iter.value().toString()); + } +} + +QByteArray ApplicationMessage::serialize() +{ + QJsonObject root; + root.insert("command", command); + QJsonObject outArgs; + for (auto iter = args.constBegin(); iter != args.constEnd(); iter++) + { + outArgs.insert(iter.key(), iter.value()); + } + root.insert("args", outArgs); + + return Json::toText(root); +} diff --git a/archived/projt-launcher/launcher/ApplicationMessage.h b/archived/projt-launcher/launcher/ApplicationMessage.h new file mode 100644 index 0000000000..8a697d1f2d --- /dev/null +++ b/archived/projt-launcher/launcher/ApplicationMessage.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include +#include +#include + +struct ApplicationMessage +{ + QString command; + QHash args; + + QByteArray serialize(); + void parse(const QByteArray& input); +}; diff --git a/archived/projt-launcher/launcher/BaseInstaller.cpp b/archived/projt-launcher/launcher/BaseInstaller.cpp new file mode 100644 index 0000000000..8f958b0f0d --- /dev/null +++ b/archived/projt-launcher/launcher/BaseInstaller.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * === Upstream License Block (Do Not Modify) ============================== + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ +#include + +#include "BaseInstaller.h" +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" + +BaseInstaller::BaseInstaller() +{} + +bool BaseInstaller::isApplied(MinecraftInstance* on) +{ + return QFile::exists(filename(on->instanceRoot())); +} + +bool BaseInstaller::add(MinecraftInstance* to) +{ + if (!patchesDir(to->instanceRoot()).exists()) + { + QDir(to->instanceRoot()).mkdir("patches"); + } + + if (isApplied(to)) + { + if (!remove(to)) + { + return false; + } + } + + return true; +} + +bool BaseInstaller::remove(MinecraftInstance* from) +{ + return FS::deletePath(filename(from->instanceRoot())); +} + +QString BaseInstaller::filename(const QString& root) const +{ + return patchesDir(root).absoluteFilePath(id() + ".json"); +} +QDir BaseInstaller::patchesDir(const QString& root) const +{ + return QDir(root + "/patches/"); +} diff --git a/archived/projt-launcher/launcher/BaseInstaller.h b/archived/projt-launcher/launcher/BaseInstaller.h new file mode 100644 index 0000000000..263c9548e0 --- /dev/null +++ b/archived/projt-launcher/launcher/BaseInstaller.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * === Upstream License Block (Do Not Modify) ============================== + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include + +#include "BaseVersion.h" + +class MinecraftInstance; +class QDir; +class QString; +class QObject; +class Task; +class BaseVersion; + +class BaseInstaller +{ + public: + BaseInstaller(); + virtual ~BaseInstaller() {}; + bool isApplied(MinecraftInstance* on); + + virtual bool add(MinecraftInstance* to); + virtual bool remove(MinecraftInstance* from); + + virtual Task* createInstallTask(MinecraftInstance* instance, BaseVersion::Ptr version, QObject* parent) = 0; + + protected: + virtual QString id() const = 0; + QString filename(const QString& root) const; + QDir patchesDir(const QString& root) const; +}; diff --git a/archived/projt-launcher/launcher/BaseInstance.cpp b/archived/projt-launcher/launcher/BaseInstance.cpp new file mode 100644 index 0000000000..f01876310b --- /dev/null +++ b/archived/projt-launcher/launcher/BaseInstance.cpp @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ======================================================================== */ + +#include "BaseInstance.h" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "Json.h" +#include "settings/INISettingsObject.h" +#include "settings/OverrideSetting.h" +#include "settings/Setting.h" + +#include "BuildConfig.h" +#include "Commandline.h" +#include "FileSystem.h" + +int getConsoleMaxLines(SettingsObjectPtr settings) +{ + auto lineSetting = settings->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) + { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + return maxLines; +} + +bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings) +{ + return settings->get("ConsoleOverflowStop").toBool(); +} + +BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) + : QObject() +{ + m_settings = settings; + m_global_settings = globalSettings; + m_rootDir = rootDir; + + m_settings->registerSetting("name", "Unnamed Instance"); + m_settings->registerSetting("iconKey", "default"); + m_settings->registerSetting("notes", ""); + + m_settings->registerSetting("lastLaunchTime", 0); + m_settings->registerSetting("totalTimePlayed", 0); + if (m_settings->get("totalTimePlayed").toLongLong() < 0) + m_settings->reset("totalTimePlayed"); + m_settings->registerSetting("lastTimePlayed", 0); + + m_settings->registerSetting("linkedInstances", "[]"); + m_settings->registerSetting("shortcuts", QString()); + + // Game time override + auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); + m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); + m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); + + // NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of + // a locally stored instance + if (!m_settings->getSetting("InstanceType")) + m_settings->registerSetting("InstanceType", ""); + + // Custom Commands + auto commandSetting = m_settings->registerSetting({ "OverrideCommands", "OverrideLaunchCmd" }, false); + m_settings->registerOverride(globalSettings->getSetting("PreLaunchCommand"), commandSetting); + m_settings->registerOverride(globalSettings->getSetting("WrapperCommand"), commandSetting); + m_settings->registerOverride(globalSettings->getSetting("PostExitCommand"), commandSetting); + + // Console + auto consoleSetting = m_settings->registerSetting("OverrideConsole", false); + m_settings->registerOverride(globalSettings->getSetting("ShowConsole"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("AutoCloseConsole"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("ShowConsoleOnError"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("LogPrePostOutput"), consoleSetting); + + m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr); + m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr); + + // Managed Packs + m_settings->registerSetting("ManagedPack", false); + m_settings->registerSetting("ManagedPackType", ""); + m_settings->registerSetting("ManagedPackID", ""); + m_settings->registerSetting("ManagedPackName", ""); + m_settings->registerSetting("ManagedPackVersionID", ""); + m_settings->registerSetting("ManagedPackVersionName", ""); + + m_settings->registerSetting("Profiler", ""); +} + +QString BaseInstance::getPreLaunchCommand() +{ + return settings()->get("PreLaunchCommand").toString(); +} + +QString BaseInstance::getWrapperCommand() +{ + return settings()->get("WrapperCommand").toString(); +} + +QString BaseInstance::getPostExitCommand() +{ + return settings()->get("PostExitCommand").toString(); +} + +bool BaseInstance::isManagedPack() const +{ + return m_settings->get("ManagedPack").toBool(); +} + +QString BaseInstance::getManagedPackType() const +{ + return m_settings->get("ManagedPackType").toString(); +} + +QString BaseInstance::getManagedPackID() const +{ + return m_settings->get("ManagedPackID").toString(); +} + +QString BaseInstance::getManagedPackName() const +{ + return m_settings->get("ManagedPackName").toString(); +} + +QString BaseInstance::getManagedPackVersionID() const +{ + return m_settings->get("ManagedPackVersionID").toString(); +} + +QString BaseInstance::getManagedPackVersionName() const +{ + return m_settings->get("ManagedPackVersionName").toString(); +} + +void BaseInstance::setManagedPack(const QString& type, + const QString& id, + const QString& name, + const QString& versionId, + const QString& version) +{ + m_settings->set("ManagedPack", true); + m_settings->set("ManagedPackType", type); + m_settings->set("ManagedPackID", id); + m_settings->set("ManagedPackName", name); + m_settings->set("ManagedPackVersionID", versionId); + m_settings->set("ManagedPackVersionName", version); +} + +void BaseInstance::copyManagedPack(BaseInstance& other) +{ + m_settings->set("ManagedPack", other.isManagedPack()); + m_settings->set("ManagedPackType", other.getManagedPackType()); + m_settings->set("ManagedPackID", other.getManagedPackID()); + m_settings->set("ManagedPackName", other.getManagedPackName()); + m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); + m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); + + if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() + && m_settings->get("OverrideJavaLocation").toBool()) + { + m_settings->set("OverrideJavaLocation", false); + m_settings->set("JavaPath", ""); + } +} + +QStringList BaseInstance::getLinkedInstances() const +{ + auto setting = m_settings->get("linkedInstances").toString(); + return Json::toStringList(setting); +} + +void BaseInstance::setLinkedInstances(const QStringList& list) +{ + m_settings->set("linkedInstances", Json::fromStringList(list)); +} + +void BaseInstance::addLinkedInstanceId(const QString& id) +{ + auto linkedInstances = getLinkedInstances(); + linkedInstances.append(id); + setLinkedInstances(linkedInstances); +} + +bool BaseInstance::removeLinkedInstanceId(const QString& id) +{ + auto linkedInstances = getLinkedInstances(); + int numRemoved = linkedInstances.removeAll(id); + setLinkedInstances(linkedInstances); + return numRemoved > 0; +} + +bool BaseInstance::isLinkedToInstanceId(const QString& id) const +{ + auto linkedInstances = getLinkedInstances(); + return linkedInstances.contains(id); +} + +void BaseInstance::iconUpdated(QString key) +{ + if (iconKey() == key) + { + emit propertiesChanged(this); + } +} + +void BaseInstance::invalidate() +{ + changeStatus(Status::Gone); + qDebug() << "Instance" << id() << "has been invalidated."; +} + +void BaseInstance::changeStatus(BaseInstance::Status newStatus) +{ + Status status = currentStatus(); + if (status != newStatus) + { + m_status = newStatus; + emit statusChanged(status, newStatus); + } +} + +BaseInstance::Status BaseInstance::currentStatus() const +{ + return m_status; +} + +QString BaseInstance::id() const +{ + return QFileInfo(instanceRoot()).fileName(); +} + +bool BaseInstance::isRunning() const +{ + return m_isRunning; +} + +void BaseInstance::setRunning(bool running) +{ + if (running == m_isRunning) + return; + + m_isRunning = running; + + emit runningStatusChanged(running); +} + +void BaseInstance::setMinecraftRunning(bool running) +{ + if (!settings()->get("RecordGameTime").toBool()) + { + return; + } + + if (running) + { + m_timeStarted = QDateTime::currentDateTime(); + setLastLaunch(m_timeStarted.toMSecsSinceEpoch()); + } + else + { + QDateTime timeEnded = QDateTime::currentDateTime(); + + qint64 current = settings()->get("totalTimePlayed").toLongLong(); + settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); + settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded)); + + emit propertiesChanged(this); + } +} + +int64_t BaseInstance::totalTimePlayed() const +{ + qint64 current = m_settings->get("totalTimePlayed").toLongLong(); + if (m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return current + m_timeStarted.secsTo(timeNow); + } + return current; +} + +int64_t BaseInstance::lastTimePlayed() const +{ + if (m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return m_timeStarted.secsTo(timeNow); + } + return m_settings->get("lastTimePlayed").toLongLong(); +} + +void BaseInstance::resetTimePlayed() +{ + settings()->reset("totalTimePlayed"); + settings()->reset("lastTimePlayed"); +} + +QString BaseInstance::instanceType() const +{ + return m_settings->get("InstanceType").toString(); +} + +QString BaseInstance::instanceRoot() const +{ + return m_rootDir; +} + +SettingsObjectPtr BaseInstance::settings() +{ + loadSpecificSettings(); + + return m_settings; +} + +bool BaseInstance::canLaunch() const +{ + return (!hasVersionBroken() && !isRunning()); +} + +bool BaseInstance::reloadSettings() +{ + return m_settings->reload(); +} + +qint64 BaseInstance::lastLaunch() const +{ + return m_settings->get("lastLaunchTime").value(); +} + +void BaseInstance::setLastLaunch(qint64 val) +{ + if (lastLaunch() == val) + return; + m_settings->set("lastLaunchTime", val); + emit propertiesChanged(this); +} + +void BaseInstance::setNotes(QString val) +{ + if (notes() == val) + return; + m_settings->set("notes", val); +} + +QString BaseInstance::notes() const +{ + return m_settings->get("notes").toString(); +} + +void BaseInstance::setIconKey(QString val) +{ + if (iconKey() == val) + return; + m_settings->set("iconKey", val); + emit propertiesChanged(this); +} + +QString BaseInstance::iconKey() const +{ + return m_settings->get("iconKey").toString(); +} + +void BaseInstance::setName(QString val) +{ + if (name() == val) + return; + m_settings->set("name", val); + emit propertiesChanged(this); +} + +bool BaseInstance::syncInstanceDirName(const QString& newRoot) const +{ + auto oldRoot = instanceRoot(); + return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); +} + +void BaseInstance::registerShortcut(const ShortcutData& data) +{ + auto currentShortcuts = shortcuts(); + currentShortcuts.append(data); + qDebug() << "Registering shortcut for instance" << id() << "with name" << data.name << "and path" << data.filePath; + setShortcuts(currentShortcuts); +} + +void BaseInstance::setShortcuts(const QList& shortcuts) +{ + // Convert new shortcuts to JSON for comparison + QJsonArray newArray; + for (const auto& elem : shortcuts) + { + newArray.append(QJsonObject{ { "name", elem.name }, + { "filePath", elem.filePath }, + { "target", static_cast(elem.target) } }); + } + QJsonDocument newDocument; + newDocument.setArray(newArray); + QString newJson = QString::fromUtf8(newDocument.toJson(QJsonDocument::Compact)); + + // Fast comparison: check if JSON strings are identical (avoids parsing and disk I/O) + QString currentJson = m_settings->get("shortcuts").toString(); + if (currentJson == newJson) + return; + + m_settings->set("shortcuts", newJson); +} + +QList BaseInstance::shortcuts() const +{ + auto data = m_settings->get("shortcuts").toString().toUtf8(); + QJsonParseError parseError; + auto document = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isArray()) + return {}; + + QList results; + for (const auto& elem : document.array()) + { + if (!elem.isObject()) + continue; + auto dict = elem.toObject(); + if (!dict.contains("name") || !dict.contains("filePath") || !dict.contains("target")) + continue; + int value = dict["target"].toInt(-1); + if (!dict["name"].isString() || !dict["filePath"].isString() || value < 0 || value >= 3) + continue; + + QString shortcutName = dict["name"].toString(); + QString filePath = dict["filePath"].toString(); + if (!QDir(filePath).exists()) + { + qWarning() << "Shortcut" << shortcutName << "for instance" << name() << "have non-existent path" + << filePath; + continue; + } + results.append({ shortcutName, filePath, static_cast(value) }); + } + return results; +} + +QString BaseInstance::name() const +{ + return m_settings->get("name").toString(); +} + +QString BaseInstance::windowTitle() const +{ + return BuildConfig.LAUNCHER_DISPLAYNAME + ": " + name(); +} + +shared_qobject_ptr BaseInstance::getLaunchPipeline() +{ + return m_launchProcess; +} + +void BaseInstance::updateRuntimeContext() +{ + // NOOP +} + +bool BaseInstance::isLegacy() +{ + return traits().contains("legacyLaunch") || traits().contains("alphaLaunch"); +} diff --git a/archived/projt-launcher/launcher/BaseInstance.h b/archived/projt-launcher/launcher/BaseInstance.h new file mode 100644 index 0000000000..e413e0162d --- /dev/null +++ b/archived/projt-launcher/launcher/BaseInstance.h @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ======================================================================== */ + +#pragma once +#include + +#include +#include +#include +#include +#include +#include +#include +#include "QObjectPtr.h" + +#include "settings/SettingsObject.h" + +#include "BaseVersionList.h" +#include "MessageLevel.h" +#include "minecraft/auth/MinecraftAccount.hpp" +#include "settings/INIFile.h" + +#include "net/Mode.h" + +#include "RuntimeContext.h" +#include "minecraft/launch/MinecraftTarget.hpp" + +class QDir; +class Task; +class InstanceWindow; +class LaunchController; + +namespace projt::launch +{ + class LaunchPipeline; +} +class BaseInstance; + +// pointer for lazy people +using InstancePtr = std::shared_ptr; + +/// Shortcut saving target representations +enum class ShortcutTarget +{ + Desktop, + Applications, + Other +}; + +/// Shortcut data representation +struct ShortcutData +{ + QString name; + QString filePath; + ShortcutTarget target = ShortcutTarget::Other; + + bool operator==(const ShortcutData& other) const + { + return name == other.name && filePath == other.filePath && target == other.target; + } +}; + +/// Console settings +int getConsoleMaxLines(SettingsObjectPtr settings); +bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings); + +/*! + * \brief Base class for instances. + * This class implements many functions that are common between instances and + * provides a standard interface for all instances. + * + * To create a new instance type, create a new class inheriting from this class + * and implement the pure virtual functions. + */ +class BaseInstance : public QObject, public std::enable_shared_from_this +{ + Q_OBJECT + protected: + /// no-touchy! + BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); + + public: /* types */ + enum class Status + { + Present, + Gone // either nuked or invalidated + }; + + public: + /// virtual destructor to make sure the destruction is COMPLETE + virtual ~BaseInstance() + {} + + virtual void saveNow() = 0; + + /*** + * the instance has been invalidated - it is no longer tracked by the launcher for some reason, + * but it has not necessarily been deleted. + * + * Happens when the instance folder changes to some other location, or the instance is removed by external means. + */ + void invalidate(); + + /// The instance's ID. The ID SHALL be determined by LAUNCHER internally. The ID IS guaranteed to + /// be unique. + virtual QString id() const; + + void setMinecraftRunning(bool running); + void setRunning(bool running); + bool isRunning() const; + int64_t totalTimePlayed() const; + int64_t lastTimePlayed() const; + void resetTimePlayed(); + + /// get the type of this instance + QString instanceType() const; + + /// Path to the instance's root directory. + QString instanceRoot() const; + + /// Path to the instance's game root directory. + virtual QString gameRoot() const + { + return instanceRoot(); + } + + /// Path to the instance's mods directory. + virtual QString modsRoot() const = 0; + + QString name() const; + void setName(QString val); + + /// Sync name and rename instance dir accordingly; returns true if successful + bool syncInstanceDirName(const QString& newRoot) const; + + /// Register a created shortcut + void registerShortcut(const ShortcutData& data); + QList shortcuts() const; + void setShortcuts(const QList& shortcuts); + + /// Value used for instance window titles + QString windowTitle() const; + + QString iconKey() const; + void setIconKey(QString val); + + QString notes() const; + void setNotes(QString val); + + QString getPreLaunchCommand(); + QString getPostExitCommand(); + QString getWrapperCommand(); + + bool isManagedPack() const; + QString getManagedPackType() const; + QString getManagedPackID() const; + QString getManagedPackName() const; + QString getManagedPackVersionID() const; + QString getManagedPackVersionName() const; + void setManagedPack(const QString& type, + const QString& id, + const QString& name, + const QString& versionId, + const QString& version); + void copyManagedPack(BaseInstance& other); + + /// Traits. Normally inside the version, depends on instance implementation. + virtual QSet traits() const = 0; + + /** + * Gets the time that the instance was last launched. + * Stored in milliseconds since epoch. + */ + qint64 lastLaunch() const; + /// Sets the last launched time to 'val' milliseconds since epoch + void setLastLaunch(qint64 val = QDateTime::currentMSecsSinceEpoch()); + + /*! + * \brief Gets this instance's settings object. + * This settings object stores instance-specific settings. + * + * Note that this method is not const. + * It may call loadSpecificSettings() to ensure those are loaded. + * + * \return A pointer to this instance's settings object. + */ + virtual SettingsObjectPtr settings(); + + /*! + * \brief Loads settings specific to an instance type if they're not already loaded. + */ + virtual void loadSpecificSettings() = 0; + + /// returns a valid update task + virtual QList createUpdateTask() = 0; + + /// returns a valid launcher (pipeline container) + virtual shared_qobject_ptr createLaunchPipeline( + AuthSessionPtr account, + MinecraftTarget::Ptr targetToJoin) = 0; + + /// returns the current launch pipeline (if any) + shared_qobject_ptr getLaunchPipeline(); + + /*! + * Create envrironment variables for running the instance + */ + virtual QProcessEnvironment createEnvironment() = 0; + virtual QProcessEnvironment createLaunchEnvironment() = 0; + + /*! + * Returns the root folder to use for looking up log files + */ + virtual QStringList getLogFileSearchPaths() = 0; + + virtual QString getStatusbarDescription() = 0; + + /// Returns the path to the instance's config directory (e.g., .minecraft/config). + /// This is on BaseInstance because all instance types need config storage, + /// even if the actual path differs per implementation. + virtual QString instanceConfigFolder() const = 0; + + /// get variables this instance exports + virtual QMap getVariables() = 0; + + virtual QString typeName() const = 0; + + virtual void updateRuntimeContext(); + RuntimeContext runtimeContext() const + { + return m_runtimeContext; + } + + bool hasVersionBroken() const + { + return m_hasBrokenVersion; + } + void setVersionBroken(bool value) + { + if (m_hasBrokenVersion != value) + { + m_hasBrokenVersion = value; + emit propertiesChanged(this); + } + } + + bool hasUpdateAvailable() const + { + return m_hasUpdate; + } + void setUpdateAvailable(bool value) + { + if (m_hasUpdate != value) + { + m_hasUpdate = value; + emit propertiesChanged(this); + } + } + + bool hasCrashed() const + { + return m_crashed; + } + void setCrashed(bool value) + { + if (m_crashed != value) + { + m_crashed = value; + emit propertiesChanged(this); + } + } + + virtual bool canLaunch() const; + virtual bool canEdit() const = 0; + virtual bool canExport() const = 0; + + bool reloadSettings(); + + /** + * 'print' a verbose description of the instance into a QStringList + */ + virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) = 0; + + Status currentStatus() const; + + QStringList getLinkedInstances() const; + void setLinkedInstances(const QStringList& list); + void addLinkedInstanceId(const QString& id); + bool removeLinkedInstanceId(const QString& id); + bool isLinkedToInstanceId(const QString& id) const; + + // Instance window and controller management + InstanceWindow* getInstanceWindow() const + { + return m_instanceWindow; + } + void setInstanceWindow(InstanceWindow* window) + { + m_instanceWindow = window; + } + + shared_qobject_ptr getLaunchController() const + { + return m_launchController; + } + void setLaunchController(shared_qobject_ptr controller) + { + m_launchController = controller; + } + + bool isLegacy(); + + protected: + void changeStatus(Status newStatus); + + SettingsObjectPtr globalSettings() const + { + return m_global_settings.lock(); + } + + bool isSpecificSettingsLoaded() const + { + return m_specific_settings_loaded; + } + void setSpecificSettingsLoaded(bool loaded) + { + m_specific_settings_loaded = loaded; + } + + signals: + /*! + * \brief Signal emitted when properties relevant to the instance view change + */ + void propertiesChanged(BaseInstance* inst); + + void launchPipelineChanged(shared_qobject_ptr); + + void runningStatusChanged(bool running); + + void profilerChanged(); + + void statusChanged(Status from, Status to); + + protected slots: + void iconUpdated(QString key); + + protected: /* data */ + QString m_rootDir; + SettingsObjectPtr m_settings; + // InstanceFlags m_flags; + bool m_isRunning = false; + shared_qobject_ptr m_launchProcess; + QDateTime m_timeStarted; + RuntimeContext m_runtimeContext; + + private: /* data */ + Status m_status = Status::Present; + bool m_crashed = false; + bool m_hasUpdate = false; + bool m_hasBrokenVersion = false; + + SettingsObjectWeakPtr m_global_settings; + bool m_specific_settings_loaded = false; + + // Instance-specific UI state (moved from Application) + InstanceWindow* m_instanceWindow = nullptr; + shared_qobject_ptr m_launchController; +}; + +Q_DECLARE_METATYPE(shared_qobject_ptr) +// Q_DECLARE_METATYPE(BaseInstance::InstanceFlag) +// Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags) diff --git a/archived/projt-launcher/launcher/BaseVersion.h b/archived/projt-launcher/launcher/BaseVersion.h new file mode 100644 index 0000000000..7200974c2e --- /dev/null +++ b/archived/projt-launcher/launcher/BaseVersion.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * === Upstream License Block (Do Not Modify) ============================== + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include +#include +#include + +/*! + * An abstract base class for versions. + */ +class BaseVersion +{ + public: + using Ptr = std::shared_ptr; + virtual ~BaseVersion() + {} + /*! + * A string used to identify this version in config files. + * This should be unique within the version list or shenanigans will occur. + */ + virtual QString descriptor() const = 0; + + /*! + * The name of this version as it is displayed to the user. + * For example: "1.5.1" + */ + virtual QString name() const = 0; + + /*! + * This should return a string that describes + * the kind of version this is (Stable, Beta, Snapshot, whatever) + */ + virtual QString typeString() const = 0; + virtual bool operator<(BaseVersion& a) const + { + return name() < a.name(); + } + virtual bool operator>(BaseVersion& a) const + { + return name() > a.name(); + } +}; + +Q_DECLARE_METATYPE(BaseVersion::Ptr) diff --git a/archived/projt-launcher/launcher/BaseVersionList.cpp b/archived/projt-launcher/launcher/BaseVersionList.cpp new file mode 100644 index 0000000000..d468a9209e --- /dev/null +++ b/archived/projt-launcher/launcher/BaseVersionList.cpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include "BaseVersionList.h" +#include "BaseVersion.h" + +BaseVersionList::BaseVersionList(QObject* parent) : QAbstractListModel(parent) +{} + +BaseVersion::Ptr BaseVersionList::findVersion(const QString& descriptor) +{ + for (int i = 0; i < count(); i++) + { + if (at(i)->descriptor() == descriptor) + return at(i); + } + return nullptr; +} + +BaseVersion::Ptr BaseVersionList::getRecommended() const +{ + if (count() <= 0) + return nullptr; + else + return at(0); +} + +QVariant BaseVersionList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + BaseVersion::Ptr version = at(index.row()); + + switch (role) + { + case VersionPointerRole: return QVariant::fromValue(version); + + case VersionRole: return version->name(); + + case VersionIdRole: return version->descriptor(); + + case TypeRole: return version->typeString(); + + case JavaMajorRole: + { + auto major = version->name(); + if (major.startsWith("java")) + { + major = "Java " + major.mid(4); + } + return major; + } + + default: return QVariant(); + } +} + +BaseVersionList::RoleList BaseVersionList::providesRoles() const +{ + return { VersionPointerRole, VersionRole, VersionIdRole, TypeRole }; +} + +int BaseVersionList::rowCount(const QModelIndex& parent) const +{ + // Return count + return parent.isValid() ? 0 : count(); +} + +int BaseVersionList::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : 1; +} + +QHash BaseVersionList::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles.insert(VersionRole, "version"); + roles.insert(VersionIdRole, "versionId"); + roles.insert(ParentVersionRole, "parentGameVersion"); + roles.insert(RecommendedRole, "recommended"); + roles.insert(LatestRole, "latest"); + roles.insert(TypeRole, "type"); + roles.insert(BranchRole, "branch"); + roles.insert(PathRole, "path"); + roles.insert(JavaNameRole, "javaName"); + roles.insert(CPUArchitectureRole, "architecture"); + roles.insert(JavaMajorRole, "javaMajor"); + return roles; +} diff --git a/archived/projt-launcher/launcher/BaseVersionList.h b/archived/projt-launcher/launcher/BaseVersionList.h new file mode 100644 index 0000000000..fa190389f5 --- /dev/null +++ b/archived/projt-launcher/launcher/BaseVersionList.h @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * === Upstream License Block (Do Not Modify) ============================== + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ +#pragma once + +#include +#include +#include + +#include "BaseVersion.h" +#include "QObjectPtr.h" +#include "tasks/Task.h" + +/*! + * \brief Class that each instance type's version list derives from. + * Version lists are the lists that keep track of the available game versions + * for that instance. This list will not be loaded on startup. It will be loaded + * when the list's load function is called. Before using the version list, you + * should check to see if it has been loaded yet and if not, load the list. + * + * Note that this class also inherits from QAbstractListModel. Methods from that + * class determine how this version list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by plugins to + * change the behavior of the list. + */ +class BaseVersionList : public QAbstractListModel +{ + Q_OBJECT + public: + enum ModelRoles + { + VersionPointerRole = Qt::UserRole, + VersionRole, + VersionIdRole, + ParentVersionRole, + RecommendedRole, + LatestRole, + TypeRole, + BranchRole, + PathRole, + JavaNameRole, + JavaMajorRole, + CPUArchitectureRole, + SortRole + }; + using RoleList = QList; + + explicit BaseVersionList(QObject* parent = 0); + + /*! + * \brief Gets a task that will reload the version list. + * Simply execute the task to load the list. + * The task returned by this function should reset the model when it's done. + * \return A pointer to a task that reloads the version list. + */ + virtual Task::Ptr getLoadTask() = 0; + + //! Checks whether or not the list is loaded. If this returns false, the list should be + // loaded. + virtual bool isLoaded() = 0; + + //! Gets the version at the given index. + virtual const BaseVersion::Ptr at(int i) const = 0; + + //! Returns the number of versions in the list. + virtual int count() const = 0; + + //////// List Model Functions //////// + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QHash roleNames() const override; + + //! which roles are provided by this version list? + virtual RoleList providesRoles() const; + + /*! + * \brief Finds a version by its descriptor. + * \param descriptor The descriptor of the version to find. + * \return A const pointer to the version with the given descriptor. NULL if + * one doesn't exist. + */ + virtual BaseVersion::Ptr findVersion(const QString& descriptor); + + /*! + * \brief Gets the recommended version from this list + * If the list doesn't support recommended versions, this works exactly as getLatestStable + */ + virtual BaseVersion::Ptr getRecommended() const; + + /*! + * Sorts the version list. + */ + virtual void sortVersions() = 0; + + protected slots: + /*! + * Updates this list with the given list of versions. + * This is done by copying each version in the given list and inserting it + * into this one. + * We need to do this so that we can set the parents of the versions are set to this + * version list. This can't be done in the load task, because the versions the load + * task creates are on the load task's thread and Qt won't allow their parents + * to be set to something created on another thread. + * To get around that problem, we invoke this method on the GUI thread, which + * then copies the versions and sets their parents correctly. + * \param versions List of versions whose parents should be set. + */ + virtual void updateListData(QList versions) = 0; +}; diff --git a/archived/projt-launcher/launcher/CMakeLists.txt b/archived/projt-launcher/launcher/CMakeLists.txt new file mode 100644 index 0000000000..9907773a70 --- /dev/null +++ b/archived/projt-launcher/launcher/CMakeLists.txt @@ -0,0 +1,2412 @@ +project(application) + +################################ FILES ################################ + +######## Sources and headers ######## + +set(CORE_SOURCES + # LOGIC - Base classes and infrastructure + BaseInstaller.h + BaseInstaller.cpp + BaseVersionList.h + BaseVersionList.cpp + InstanceList.h + InstanceList.cpp + InstanceTask.h + InstanceTask.cpp + LoggedProcess.h + LoggedProcess.cpp + MessageLevel.cpp + MessageLevel.h + BaseVersion.h + BaseInstance.h + BaseInstance.cpp + InstanceDirUpdate.h + InstanceDirUpdate.cpp + NullInstance.h + MMCZip.h + MMCZip.cpp + Untar.h + Untar.cpp + StringUtils.h + StringUtils.cpp + QVariantUtils.h + RuntimeContext.h + PSaveFile.h + + # Basic instance manipulation tasks (derived from InstanceTask) + InstanceCreationTask.h + InstanceCreationTask.cpp + InstanceCopyPrefs.h + InstanceCopyPrefs.cpp + InstanceCopyTask.h + InstanceCopyTask.cpp + InstanceImportTask.h + InstanceImportTask.cpp + + # Resource downloading task + ResourceDownloadTask.h + ResourceDownloadTask.cpp + + # Use tracking separate from memory management + Usable.h + + # Prefix tree where node names are strings between separators + SeparatorPrefixTree.h + + # String filters + Filter.h + + # JSON parsing helpers + Json.h + Json.cpp + + FileSystem.h + FileSystem.cpp + + Exception.h + + # RW lock protected map + RWStorage.h + + # A variable that has an implicit default value and keeps track of changes + DefaultVariable.h + + # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms + QObjectPtr.h + + # Compression support + GZip.h + GZip.cpp + + # Command line parameter parsing + Commandline.h + Commandline.cpp + + # Version number string support + Version.h + Version.cpp + + # A Recursive file system watcher + RecursiveFileSystemWatcher.h + RecursiveFileSystemWatcher.cpp + + # Time + MMCTime.h + MMCTime.cpp + + MTPixmapCache.h +) +if (UNIX AND NOT CYGWIN AND NOT APPLE) + set(CORE_SOURCES + ${CORE_SOURCES} + + # MangoHud + MangoHud.h + MangoHud.cpp + ) +endif() + +set(NET_SOURCES + # network stuffs + net/ByteArraySink.h + net/ChecksumValidator.h + net/Download.cpp + net/Download.h + net/FileSink.cpp + net/FileSink.h + net/HttpMetaCache.cpp + net/HttpMetaCache.h + net/MetaCacheSink.cpp + net/MetaCacheSink.h + net/Logging.h + net/Logging.cpp + net/NetJob.cpp + net/NetJob.h + net/NetUtils.h + net/PasteUpload.cpp + net/PasteUpload.h + net/Sink.h + net/Validator.h + net/Upload.cpp + net/Upload.h + net/HeaderProxy.h + net/RawHeaderProxy.h + net/ApiHeaderProxy.h + net/ApiDownload.h + net/ApiDownload.cpp + net/ApiUpload.cpp + net/ApiUpload.h + net/NetRequest.cpp + net/NetRequest.h +) + +# Game launch logic +set(LAUNCH_SOURCES + launch/steps/RuntimeProbeStep.cpp + launch/steps/RuntimeProbeStep.hpp + launch/steps/ServerJoinResolveStep.cpp + launch/steps/ServerJoinResolveStep.hpp + launch/steps/LaunchCommandStep.cpp + launch/steps/LaunchCommandStep.hpp + launch/steps/LogMessageStep.cpp + launch/steps/LogMessageStep.hpp + launch/steps/QuitAfterGameStep.cpp + launch/steps/QuitAfterGameStep.hpp + launch/steps/HostLookupReportStep.cpp + launch/steps/HostLookupReportStep.hpp + launch/LaunchStage.cpp + launch/LaunchStage.hpp + launch/LaunchPipeline.cpp + launch/LaunchPipeline.hpp + launch/LaunchLineRouter.cpp + launch/LaunchLineRouter.hpp + launch/LaunchLogModel.cpp + launch/LaunchLogModel.hpp + launch/TaskBridgeStage.cpp + launch/TaskBridgeStage.hpp + launch/LaunchVariableExpander.cpp + launch/LaunchVariableExpander.hpp + logs/LogEventParser.cpp + logs/LogEventParser.hpp +) + +# Old update system +set(UPDATE_SOURCES + updater/ExternalUpdater.h +) + +set(MAC_UPDATE_SOURCES + updater/MacSparkleUpdater.h + updater/MacSparkleUpdater.mm +) + +set(PROJT_UPDATE_SOURCES + updater/ProjTExternalUpdater.h + updater/ProjTExternalUpdater.cpp +) + +# Backend for the news bar... there's usually no news. +set(NEWS_SOURCES + # News System + news/NewsChecker.h + news/NewsChecker.cpp + news/NewsEntry.h + news/NewsEntry.cpp +) + +# Icon interface +set(ICONS_SOURCES + # Icons System and related code + icons/IconUtils.hpp + icons/IconUtils.cpp +) + +# Support for Minecraft instances and launch +set(MINECRAFT_SOURCES + + # Logging + minecraft/Logging.h + minecraft/Logging.cpp + + # Backup system + minecraft/BackupManager.h + minecraft/BackupManager.cpp + + # Minecraft support + minecraft/auth/AccountData.cpp + minecraft/auth/AccountData.hpp + minecraft/auth/AccountList.cpp + minecraft/auth/AccountList.hpp + minecraft/auth/AuthSession.cpp + minecraft/auth/AuthSession.hpp + minecraft/auth/MinecraftAccount.cpp + minecraft/auth/MinecraftAccount.hpp + minecraft/auth/Parsers.cpp + minecraft/auth/Parsers.hpp + + minecraft/auth/AuthFlow.cpp + minecraft/auth/AuthFlow.hpp + + # New auth steps (projt::minecraft::auth namespace) + minecraft/auth/steps/Step.hpp + minecraft/auth/steps/Credentials.hpp + minecraft/auth/steps/Steps.hpp + minecraft/auth/steps/MicrosoftOAuthStep.hpp + minecraft/auth/steps/MicrosoftOAuthStep.cpp + minecraft/auth/steps/DeviceCodeAuthStep.hpp + minecraft/auth/steps/DeviceCodeAuthStep.cpp + minecraft/auth/steps/XboxLiveUserStep.hpp + minecraft/auth/steps/XboxLiveUserStep.cpp + minecraft/auth/steps/XboxSecurityTokenStep.hpp + minecraft/auth/steps/XboxSecurityTokenStep.cpp + minecraft/auth/steps/XboxProfileFetchStep.hpp + minecraft/auth/steps/XboxProfileFetchStep.cpp + minecraft/auth/steps/MinecraftServicesLoginStep.hpp + minecraft/auth/steps/MinecraftServicesLoginStep.cpp + minecraft/auth/steps/MinecraftProfileFetchStep.hpp + minecraft/auth/steps/MinecraftProfileFetchStep.cpp + minecraft/auth/steps/GameEntitlementsStep.hpp + minecraft/auth/steps/GameEntitlementsStep.cpp + minecraft/auth/steps/SkinDownloadStep.hpp + minecraft/auth/steps/SkinDownloadStep.cpp + + minecraft/update/AssetUpdateTask.h + minecraft/update/AssetUpdateTask.cpp + minecraft/update/FMLLibrariesTask.cpp + minecraft/update/FMLLibrariesTask.h + minecraft/update/FoldersTask.cpp + minecraft/update/FoldersTask.h + minecraft/update/LibrariesTask.cpp + minecraft/update/LibrariesTask.h + + minecraft/launch/ClaimAccount.cpp + minecraft/launch/ClaimAccount.hpp + minecraft/launch/CreateGameFolders.cpp + minecraft/launch/CreateGameFolders.hpp + minecraft/launch/ModMinecraftJar.cpp + minecraft/launch/ModMinecraftJar.hpp + minecraft/launch/ExtractNatives.cpp + minecraft/launch/ExtractNatives.hpp + minecraft/launch/LauncherPartLaunch.cpp + minecraft/launch/LauncherPartLaunch.hpp + minecraft/launch/MinecraftTarget.cpp + minecraft/launch/MinecraftTarget.hpp + minecraft/launch/PrintInstanceInfo.cpp + minecraft/launch/PrintInstanceInfo.hpp + minecraft/launch/ReconstructAssets.cpp + minecraft/launch/ReconstructAssets.hpp + minecraft/launch/ScanModFolders.cpp + minecraft/launch/ScanModFolders.hpp + minecraft/launch/VerifyJavaInstall.cpp + minecraft/launch/VerifyJavaInstall.hpp + minecraft/launch/AutoInstallJava.cpp + minecraft/launch/AutoInstallJava.hpp + + minecraft/GradleSpecifier.h + minecraft/MinecraftInstance.cpp + minecraft/MinecraftInstance.h + minecraft/MinecraftInstanceLaunchMenu.cpp + minecraft/MinecraftInstanceLaunchMenu.h + minecraft/LaunchProfile.cpp + minecraft/LaunchProfile.h + minecraft/Component.cpp + minecraft/Component.h + minecraft/PackProfile.cpp + minecraft/PackProfile.h + minecraft/ComponentUpdateTask.cpp + minecraft/ComponentUpdateTask.h + minecraft/MinecraftLoadAndCheck.h + minecraft/MinecraftLoadAndCheck.cpp + minecraft/MojangVersionFormat.cpp + minecraft/MojangVersionFormat.h + minecraft/Rule.cpp + minecraft/Rule.h + minecraft/OneSixVersionFormat.cpp + minecraft/OneSixVersionFormat.h + minecraft/ParseUtils.cpp + minecraft/ParseUtils.h + minecraft/ProfileUtils.cpp + minecraft/ProfileUtils.h + minecraft/ShortcutUtils.cpp + minecraft/ShortcutUtils.h + minecraft/Library.cpp + minecraft/Library.h + minecraft/MojangDownloadInfo.h + minecraft/VanillaInstanceCreationTask.cpp + minecraft/VanillaInstanceCreationTask.h + minecraft/VersionFile.cpp + minecraft/VersionFile.h + minecraft/VersionFilterData.h + minecraft/VersionFilterData.cpp + minecraft/World.h + minecraft/World.cpp + minecraft/WorldList.h + minecraft/WorldList.cpp + + minecraft/mod/MetadataHandler.hpp + minecraft/mod/Mod.hpp + minecraft/mod/Mod.cpp + minecraft/mod/ModDetails.hpp + minecraft/mod/ModFolderModel.hpp + minecraft/mod/ModFolderModel.cpp + minecraft/mod/Resource.hpp + minecraft/mod/Resource.cpp + minecraft/mod/ResourceFolderModel.hpp + minecraft/mod/ResourceFolderModel.cpp + minecraft/mod/DataPack.hpp + minecraft/mod/DataPack.cpp + minecraft/mod/DataPackFolderModel.hpp + minecraft/mod/DataPackFolderModel.cpp + minecraft/mod/ResourcePack.hpp + minecraft/mod/ResourcePack.cpp + minecraft/mod/ResourcePackFolderModel.hpp + minecraft/mod/ResourcePackFolderModel.cpp + minecraft/mod/TexturePack.hpp + minecraft/mod/TexturePack.cpp + minecraft/mod/ShaderPack.hpp + minecraft/mod/ShaderPack.cpp + minecraft/mod/WorldSave.hpp + minecraft/mod/WorldSave.cpp + minecraft/mod/TexturePackFolderModel.hpp + minecraft/mod/TexturePackFolderModel.cpp + minecraft/mod/ShaderPackFolderModel.hpp + minecraft/mod/tasks/ResourceFolderLoadTask.hpp + minecraft/mod/tasks/ResourceFolderLoadTask.cpp + minecraft/mod/tasks/LocalModParseTask.hpp + minecraft/mod/tasks/LocalModParseTask.cpp + minecraft/mod/tasks/LocalResourceUpdateTask.hpp + minecraft/mod/tasks/LocalResourceUpdateTask.cpp + minecraft/mod/tasks/LocalDataPackParseTask.hpp + minecraft/mod/tasks/LocalDataPackParseTask.cpp + minecraft/mod/tasks/LocalTexturePackParseTask.hpp + minecraft/mod/tasks/LocalTexturePackParseTask.cpp + minecraft/mod/tasks/LocalShaderPackParseTask.hpp + minecraft/mod/tasks/LocalShaderPackParseTask.cpp + minecraft/mod/tasks/LocalWorldSaveParseTask.hpp + minecraft/mod/tasks/LocalWorldSaveParseTask.cpp + minecraft/mod/tasks/LocalResourceParse.hpp + minecraft/mod/tasks/LocalResourceParse.cpp + minecraft/mod/tasks/GetModDependenciesTask.hpp + minecraft/mod/tasks/GetModDependenciesTask.cpp + + # Assets + minecraft/AssetsUtils.h + minecraft/AssetsUtils.cpp + + # Minecraft skins + minecraft/skins/CapeChange.cpp + minecraft/skins/CapeChange.h + minecraft/skins/CapeListModel.cpp + minecraft/skins/CapeListModel.h + minecraft/skins/SkinUpload.cpp + minecraft/skins/SkinUpload.h + minecraft/skins/SkinDelete.cpp + minecraft/skins/SkinDelete.h + minecraft/skins/SkinModel.cpp + minecraft/skins/SkinModel.h + minecraft/skins/SkinList.cpp + minecraft/skins/SkinList.h + + minecraft/Agent.h) + +# the screenshots feature +set(SCREENSHOTS_SOURCES + screenshots/Screenshot.h + screenshots/ImgurUpload.h + screenshots/ImgurUpload.cpp + screenshots/ImgurAlbumCreation.h + screenshots/ImgurAlbumCreation.cpp +) + +set(TASKS_SOURCES + # Tasks + tasks/Task.h + tasks/Task.cpp + tasks/ConcurrentTask.h + tasks/ConcurrentTask.cpp + tasks/SequentialTask.h + tasks/SequentialTask.cpp + tasks/MultipleOptionsTask.h + tasks/MultipleOptionsTask.cpp +) + +set(SETTINGS_SOURCES + # Settings + settings/INIFile.cpp + settings/INIFile.h + settings/INISettingsObject.cpp + settings/INISettingsObject.h + settings/OverrideSetting.cpp + settings/OverrideSetting.h + settings/PassthroughSetting.cpp + settings/PassthroughSetting.h + settings/Setting.cpp + settings/Setting.h + settings/SettingsObject.cpp + settings/SettingsObject.h +) + +set(JAVA_SOURCES + java/core/RuntimeVersion.hpp + java/core/RuntimeVersion.cpp + java/core/RuntimeInstall.hpp + java/core/RuntimeInstall.cpp + java/core/RuntimePackage.hpp + java/core/RuntimePackage.cpp + java/services/RuntimeEnvironment.hpp + java/services/RuntimeEnvironment.cpp + java/services/RuntimeScanner.hpp + java/services/RuntimeScanner.cpp + java/services/RuntimeProbeTask.hpp + java/services/RuntimeProbeTask.cpp + java/services/RuntimeCatalog.hpp + java/services/RuntimeCatalog.cpp + java/download/RuntimeArchiveTask.cpp + java/download/RuntimeArchiveTask.hpp + java/download/RuntimeManifestTask.cpp + java/download/RuntimeManifestTask.hpp + java/download/RuntimeLinkTask.cpp + java/download/RuntimeLinkTask.hpp + + ui/java/InstallJavaDialog.h + ui/java/InstallJavaDialog.cpp + ui/java/VersionList.h + ui/java/VersionList.cpp +) + +set(TRANSLATIONS_SOURCES + translations/TranslationsModel.h + translations/TranslationsModel.cpp + translations/POTranslator.h + translations/POTranslator.cpp +) + +set(TOOLS_SOURCES + # Tools + tools/BaseExternalTool.cpp + tools/BaseExternalTool.h + tools/BaseProfiler.cpp + tools/BaseProfiler.h + tools/JProfiler.cpp + tools/JProfiler.h + tools/JVisualVM.cpp + tools/JVisualVM.h + tools/MCEditTool.cpp + tools/MCEditTool.h + tools/GenericProfiler.cpp + tools/GenericProfiler.h +) + +set(META_SOURCES + # Metadata sources (projt::meta namespace) + meta/BaseEntity.cpp + meta/BaseEntity.hpp + meta/JsonFormat.cpp + meta/JsonFormat.hpp + meta/Version.cpp + meta/Version.hpp + meta/VersionList.cpp + meta/VersionList.hpp + meta/Index.cpp + meta/Index.hpp +) + +set(API_SOURCES + modplatform/ModIndex.h + modplatform/ModIndex.cpp + modplatform/ResourceType.h + modplatform/ResourceType.cpp + + modplatform/ResourceAPI.h + modplatform/ResourceAPI.cpp + + modplatform/EnsureMetadataTask.h + modplatform/EnsureMetadataTask.cpp + + modplatform/CheckUpdateTask.h + + modplatform/flame/FlameAPI.h + modplatform/flame/FlameAPI.cpp + modplatform/modrinth/ModrinthAPI.h + modplatform/modrinth/ModrinthAPI.cpp + modplatform/helpers/HashUtils.h + modplatform/helpers/HashUtils.cpp + modplatform/helpers/OverrideUtils.h + modplatform/helpers/OverrideUtils.cpp + + modplatform/helpers/ExportToModList.h + modplatform/helpers/ExportToModList.cpp +) + +set(FTB_SOURCES + modplatform/legacy_ftb/PackFetchTask.h + modplatform/legacy_ftb/PackFetchTask.cpp + modplatform/legacy_ftb/PackInstallTask.h + modplatform/legacy_ftb/PackInstallTask.cpp + modplatform/legacy_ftb/PrivatePackManager.h + modplatform/legacy_ftb/PrivatePackManager.cpp + + modplatform/legacy_ftb/PackHelpers.h + + modplatform/import_ftb/PackInstallTask.h + modplatform/import_ftb/PackInstallTask.cpp + modplatform/import_ftb/PackHelpers.h + modplatform/import_ftb/PackHelpers.cpp +) + +set(FLAME_SOURCES + # Flame + modplatform/flame/FlameModIndex.cpp + modplatform/flame/FlameModIndex.h + modplatform/flame/PackManifest.h + modplatform/flame/PackManifest.cpp + modplatform/flame/FileResolvingTask.h + modplatform/flame/FileResolvingTask.cpp + modplatform/flame/FlameCheckUpdate.cpp + modplatform/flame/FlameCheckUpdate.h + modplatform/flame/FlameInstanceCreationTask.h + modplatform/flame/FlameInstanceCreationTask.cpp + modplatform/flame/FlamePackExportTask.h + modplatform/flame/FlamePackExportTask.cpp +) + +set(MODRINTH_SOURCES + modplatform/modrinth/ModrinthPackIndex.cpp + modplatform/modrinth/ModrinthPackIndex.h + modplatform/modrinth/ModrinthCollectionImportTask.cpp + modplatform/modrinth/ModrinthCollectionImportTask.h + modplatform/modrinth/ModrinthCheckUpdate.cpp + modplatform/modrinth/ModrinthCheckUpdate.h + modplatform/modrinth/ModrinthInstanceCreationTask.cpp + modplatform/modrinth/ModrinthInstanceCreationTask.h + modplatform/modrinth/ModrinthPackExportTask.cpp + modplatform/modrinth/ModrinthPackExportTask.h +) + +set(PACKWIZ_SOURCES + modplatform/packwiz/Packwiz.h + modplatform/packwiz/Packwiz.cpp +) + + +set(TECHNIC_SOURCES + modplatform/technic/SingleZipPackInstallTask.h + modplatform/technic/SingleZipPackInstallTask.cpp + modplatform/technic/SolderPackInstallTask.h + modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/SolderPackManifest.h + modplatform/technic/SolderPackManifest.cpp + modplatform/technic/TechnicPackProcessor.h + modplatform/technic/TechnicPackProcessor.cpp +) + +set(ATLAUNCHER_SOURCES + modplatform/atlauncher/ATLPackIndex.cpp + modplatform/atlauncher/ATLPackIndex.h + modplatform/atlauncher/ATLPackInstallTask.cpp + modplatform/atlauncher/ATLPackInstallTask.h + modplatform/atlauncher/ATLPackManifest.cpp + modplatform/atlauncher/ATLPackManifest.h + modplatform/atlauncher/ATLShareCode.cpp + modplatform/atlauncher/ATLShareCode.h +) + +set(LINKEXE_SOURCES + console/WindowsConsole.hpp + console/WindowsConsole.cpp + + filelink/FileLink.hpp + filelink/FileLink.cpp + FileSystem.h + FileSystem.cpp + Exception.h + StringUtils.h + StringUtils.cpp + DesktopServices.h + DesktopServices.cpp +) + +set(PROJTUPDATER_SOURCES + updater/projtupdater/ProjTUpdater.h + updater/projtupdater/ProjTUpdater.cpp + updater/projtupdater/UpdaterDialogs.h + updater/projtupdater/UpdaterDialogs.cpp + updater/projtupdater/ReleaseInfo.h + updater/projtupdater/ReleaseInfo.cpp + + Json.h + Json.cpp + FileSystem.h + FileSystem.cpp + StringUtils.h + StringUtils.cpp + DesktopServices.h + DesktopServices.cpp + Version.h + Version.cpp + Markdown.h + Markdown.cpp + + # Zip + MMCZip.h + MMCZip.cpp + + # Time + MMCTime.h + MMCTime.cpp + + net/ByteArraySink.h + net/ChecksumValidator.h + net/Download.cpp + net/Download.h + net/FileSink.cpp + net/FileSink.h + net/HttpMetaCache.cpp + net/HttpMetaCache.h + net/Logging.h + net/Logging.cpp + net/NetRequest.cpp + net/NetRequest.h + net/NetJob.cpp + net/NetJob.h + net/NetUtils.h + net/Sink.h + net/Validator.h + net/HeaderProxy.h + net/RawHeaderProxy.h + + ui/dialogs/ProgressDialog.cpp + ui/dialogs/ProgressDialog.h + ui/widgets/SubTaskProgressBar.h + ui/widgets/SubTaskProgressBar.cpp + +) + +if(WIN32) + set(PROJTUPDATER_SOURCES + console/WindowsConsole.hpp + console/WindowsConsole.cpp + ${PROJTUPDATER_SOURCES} + ) +endif() + +######## Logging categories ######## + +ecm_qt_export_logging_category( + IDENTIFIER authCredentials + CATEGORY_NAME "launcher.auth.credentials" + DEFAULT_SEVERITY Warning + DESCRIPTION "Secrets and credentials for debugging purposes" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER instanceProfileC + CATEGORY_NAME "launcher.instance.profile" + DEFAULT_SEVERITY Debug + DESCRIPTION "Profile actions" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER instanceProfileResolveC + CATEGORY_NAME "launcher.instance.profile.resolve" + DEFAULT_SEVERITY Debug + DESCRIPTION "Profile component resolusion actions" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER taskLogC + CATEGORY_NAME "launcher.task" + DEFAULT_SEVERITY Debug + DESCRIPTION "Task actions" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER taskNetLogC + CATEGORY_NAME "launcher.task.net" + DEFAULT_SEVERITY Debug + DESCRIPTION "Task network action" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER taskDownloadLogC + CATEGORY_NAME "launcher.task.net.download" + DEFAULT_SEVERITY Debug + DESCRIPTION "Task network download actions" + EXPORT "${Launcher_Name}" +) +ecm_qt_export_logging_category( + IDENTIFIER taskUploadLogC + CATEGORY_NAME "launcher.task.net.upload" + DEFAULT_SEVERITY Debug + DESCRIPTION "Task network upload actions" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER taskMetaCacheLogC + CATEGORY_NAME "launcher.task.net.metacache" + DEFAULT_SEVERITY Debug + DESCRIPTION "task network meta-cache actions" + EXPORT "${Launcher_Name}" +) + +ecm_qt_export_logging_category( + IDENTIFIER taskHttpMetaCacheLogC + CATEGORY_NAME "launcher.task.net.metacache.http" + DEFAULT_SEVERITY Debug + DESCRIPTION "task network http meta-cache actions" + EXPORT "${Launcher_Name}" +) + + + +if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this + ecm_qt_install_logging_categories( + EXPORT "${Launcher_Name}" + DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}" + ) +endif() + +################################ COMPILE ################################ + +set(LOGIC_SOURCES + ${CORE_SOURCES} + ${NET_SOURCES} + ${LAUNCH_SOURCES} + ${UPDATE_SOURCES} + ${NEWS_SOURCES} + ${MINECRAFT_SOURCES} + ${SCREENSHOTS_SOURCES} + ${TASKS_SOURCES} + ${SETTINGS_SOURCES} + ${JAVA_SOURCES} + ${TRANSLATIONS_SOURCES} + ${TOOLS_SOURCES} + ${META_SOURCES} + ${ICONS_SOURCES} + ${API_SOURCES} + ${FTB_SOURCES} + ${FLAME_SOURCES} + ${MODRINTH_SOURCES} + ${PACKWIZ_SOURCES} + ${TECHNIC_SOURCES} + ${ATLAUNCHER_SOURCES} +) + +if(APPLE AND Launcher_ENABLE_UPDATER) + set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) +else() + set (LOGIC_SOURCES ${LOGIC_SOURCES} ${PROJT_UPDATE_SOURCES}) +endif() + +SET(LAUNCHER_SOURCES + # Application base + Application.h + Application.cpp + DataMigrationTask.h + DataMigrationTask.cpp + ApplicationMessage.h + ApplicationMessage.cpp + SysInfo.h + SysInfo.cpp + HardwareInfo.cpp + HardwareInfo.h + + # console utils + console/Console.hpp + + # GUI - general utilities + DesktopServices.h + DesktopServices.cpp + VersionProxyModel.h + VersionProxyModel.cpp + Markdown.h + Markdown.cpp + + # Super secret! + KonamiCode.h + KonamiCode.cpp + + # Bundled resources + resources/backgrounds/backgrounds.qrc + resources/multimc/multimc.qrc + resources/pe_dark/pe_dark.qrc + resources/pe_light/pe_light.qrc + resources/pe_colored/pe_colored.qrc + resources/pe_blue/pe_blue.qrc + resources/breeze_dark/breeze_dark.qrc + resources/breeze_light/breeze_light.qrc + resources/OSX/OSX.qrc + resources/iOS/iOS.qrc + resources/flat/flat.qrc + resources/flat_white/flat_white.qrc + resources/documents/documents.qrc + resources/shaders/shaders.qrc + "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" + + # Icons + icons/IconEntry.hpp + icons/IconEntry.cpp + icons/IconList.hpp + icons/IconList.cpp + + # log utils + logs/LogRedactor.cpp + logs/LogRedactor.hpp + + # GUI - windows + ui/GuiUtil.h + ui/GuiUtil.cpp + ui/MainWindow.h + ui/MainWindow.cpp + ui/InstanceWindow.h + ui/InstanceWindow.cpp + ui/ViewLogWindow.h + ui/ViewLogWindow.cpp + ui/LaunchMenu.cpp + ui/LaunchMenu.h + + # GUI - widgets / utilities + ui/widgets/FileIgnoreProxy.cpp + ui/widgets/FileIgnoreProxy.h + CefRuntime.cpp + CefRuntime.h + ui/widgets/HubViewBase.h + ui/widgets/FastFileIconProvider.cpp + ui/widgets/FastFileIconProvider.h + ui/widgets/FallbackHubView.cpp + ui/widgets/FallbackHubView.h + ui/widgets/CefHubView.cpp + ui/widgets/CefHubView.h + ui/widgets/HubSearchProvider.cpp + ui/widgets/HubSearchProvider.h + ui/widgets/LauncherHubWidget.cpp + ui/widgets/LauncherHubWidget.h + ui/widgets/QtWebEngineHubView.cpp + ui/widgets/QtWebEngineHubView.h + ui/widgets/WebView2Widget.cpp + ui/widgets/WebView2Widget.h + + # GUI - setup wizard + ui/setupwizard/SetupWizard.h + ui/setupwizard/SetupWizard.cpp + ui/setupwizard/BaseWizardPage.h + ui/setupwizard/JavaWizardPage.cpp + ui/setupwizard/JavaWizardPage.h + ui/setupwizard/LanguageWizardPage.cpp + ui/setupwizard/LanguageWizardPage.h + ui/setupwizard/SearchWizardPage.cpp + ui/setupwizard/SearchWizardPage.h + ui/setupwizard/PasteWizardPage.cpp + ui/setupwizard/PasteWizardPage.h + ui/setupwizard/ThemeWizardPage.h + ui/setupwizard/AutoJavaWizardPage.cpp + ui/setupwizard/AutoJavaWizardPage.h + ui/setupwizard/LoginWizardPage.cpp + ui/setupwizard/LoginWizardPage.h + + # GUI - themes + ui/themes/FusionTheme.cpp + ui/themes/FusionTheme.h + ui/themes/BrightTheme.cpp + ui/themes/BrightTheme.h + ui/themes/CustomTheme.cpp + ui/themes/CustomTheme.h + ui/themes/DarkTheme.cpp + ui/themes/DarkTheme.h + ui/themes/Theme.cpp + ui/themes/Theme.h + ui/themes/HintOverrideProxyStyle.cpp + ui/themes/HintOverrideProxyStyle.h + ui/themes/SystemTheme.cpp + ui/themes/SystemTheme.h + ui/themes/IconTheme.cpp + ui/themes/IconTheme.h + ui/themes/ThemeManager.cpp + ui/themes/ThemeManager.h + ui/themes/CatPack.cpp + ui/themes/CatPack.h + ui/themes/CatPainter.cpp + ui/themes/CatPainter.h + + # Processes + LaunchMode.h + LaunchController.h + LaunchController.cpp + + # page provider for instances + InstancePageProvider.h + + # Common java checking UI + JavaCommon.h + JavaCommon.cpp + + # GUI - paged dialog base + ui/pages/BasePage.h + ui/pages/BasePageContainer.h + ui/pages/BasePageProvider.h + + # GUI - instance pages + ui/pages/instance/ExternalResourcesPage.cpp + ui/pages/instance/ExternalResourcesPage.h + ui/pages/instance/VersionPage.cpp + ui/pages/instance/VersionPage.h + ui/pages/instance/ManagedPackPage.cpp + ui/pages/instance/ManagedPackPage.h + ui/pages/instance/DataPackPage.h + ui/pages/instance/DataPackPage.cpp + ui/pages/instance/TexturePackPage.h + ui/pages/instance/TexturePackPage.cpp + ui/pages/instance/ResourcePackPage.h + ui/pages/instance/ResourcePackPage.cpp + ui/pages/instance/ShaderPackPage.h + ui/pages/instance/ShaderPackPage.cpp + ui/pages/instance/ModFolderPage.cpp + ui/pages/instance/ModFolderPage.h + ui/pages/instance/NotesPage.cpp + ui/pages/instance/NotesPage.h + ui/pages/instance/LogPage.cpp + ui/pages/instance/LogPage.h + ui/pages/instance/InstanceSettingsPage.h + ui/pages/instance/BackupPage.cpp + ui/pages/instance/BackupPage.h + ui/pages/instance/ScreenshotsPage.cpp + ui/pages/instance/ScreenshotsPage.h + ui/pages/instance/OtherLogsPage.cpp + ui/pages/instance/OtherLogsPage.h + ui/pages/instance/ServersPage.cpp + ui/pages/instance/ServersPage.h + ui/pages/instance/WorldListPage.cpp + ui/pages/instance/WorldListPage.h + ui/pages/instance/McClient.cpp + ui/pages/instance/McClient.h + ui/pages/instance/McResolver.cpp + ui/pages/instance/McResolver.h + ui/pages/instance/ServerPingTask.cpp + ui/pages/instance/ServerPingTask.h + + # GUI - global settings pages + ui/pages/global/AccountListPage.cpp + ui/pages/global/AccountListPage.h + ui/pages/global/ExternalToolsPage.cpp + ui/pages/global/ExternalToolsPage.h + ui/pages/global/JavaPage.cpp + ui/pages/global/JavaPage.h + ui/pages/global/LanguagePage.cpp + ui/pages/global/LanguagePage.h + ui/pages/global/MinecraftPage.h + ui/pages/global/LauncherPage.cpp + ui/pages/global/LauncherPage.h + ui/pages/global/AppearancePage.h + ui/pages/global/ProxyPage.cpp + ui/pages/global/ProxyPage.h + ui/pages/global/APIPage.cpp + ui/pages/global/APIPage.h + + # GUI - platform pages + ui/pages/modplatform/CustomPage.cpp + ui/pages/modplatform/CustomPage.h + + ui/pages/modplatform/ResourcePage.cpp + ui/pages/modplatform/ResourcePage.h + ui/pages/modplatform/ResourceModel.cpp + ui/pages/modplatform/ResourceModel.h + + ui/pages/modplatform/ModPage.cpp + ui/pages/modplatform/ModPage.h + ui/pages/modplatform/ModModel.cpp + ui/pages/modplatform/ModModel.h + + ui/pages/modplatform/ResourcePackPage.cpp + ui/pages/modplatform/ResourcePackModel.cpp + + # Needed for MOC to find them without a corresponding .cpp + ui/pages/modplatform/TexturePackPage.h + ui/pages/modplatform/TexturePackModel.cpp + + ui/pages/modplatform/ShaderPackPage.cpp + ui/pages/modplatform/ShaderPackModel.cpp + + ui/pages/modplatform/DataPackPage.cpp + ui/pages/modplatform/DataPackModel.cpp + + ui/pages/modplatform/ModpackProviderBasePage.h + + ui/pages/modplatform/atlauncher/AtlFilterModel.cpp + ui/pages/modplatform/atlauncher/AtlFilterModel.h + ui/pages/modplatform/atlauncher/AtlListModel.cpp + ui/pages/modplatform/atlauncher/AtlListModel.h + ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp + ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h + ui/pages/modplatform/atlauncher/AtlPage.cpp + ui/pages/modplatform/atlauncher/AtlPage.h + ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp + ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h + + ui/pages/modplatform/legacy_ftb/Page.cpp + ui/pages/modplatform/legacy_ftb/Page.h + ui/pages/modplatform/legacy_ftb/ListModel.h + ui/pages/modplatform/legacy_ftb/ListModel.cpp + + ui/pages/modplatform/import_ftb/ImportFTBPage.cpp + ui/pages/modplatform/import_ftb/ImportFTBPage.h + ui/pages/modplatform/import_ftb/ListModel.h + ui/pages/modplatform/import_ftb/ListModel.cpp + + ui/pages/modplatform/flame/FlameModel.cpp + ui/pages/modplatform/flame/FlameModel.h + ui/pages/modplatform/flame/FlamePage.cpp + ui/pages/modplatform/flame/FlamePage.h + ui/pages/modplatform/flame/FlameResourceModels.cpp + ui/pages/modplatform/flame/FlameResourceModels.h + ui/pages/modplatform/flame/FlameResourcePages.cpp + ui/pages/modplatform/flame/FlameResourcePages.h + + ui/pages/modplatform/modrinth/ModrinthPage.cpp + ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/modrinth/ModrinthModel.cpp + ui/pages/modplatform/modrinth/ModrinthModel.h + + ui/pages/modplatform/technic/TechnicModel.cpp + ui/pages/modplatform/technic/TechnicModel.h + ui/pages/modplatform/technic/TechnicPage.cpp + ui/pages/modplatform/technic/TechnicPage.h + + ui/pages/modplatform/ImportPage.cpp + ui/pages/modplatform/ImportPage.h + + ui/pages/modplatform/OptionalModDialog.cpp + ui/pages/modplatform/OptionalModDialog.h + + ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp + ui/pages/modplatform/modrinth/ModrinthResourcePages.h + + # GUI - dialogs + ui/dialogs/AboutDialog.cpp + ui/dialogs/AboutDialog.h + ui/dialogs/BackupDialog.cpp + ui/dialogs/BackupDialog.h + ui/dialogs/ProfileSelectDialog.cpp + ui/dialogs/ProfileSelectDialog.h + ui/dialogs/ProfileSetupDialog.cpp + ui/dialogs/ProfileSetupDialog.h + ui/dialogs/CopyInstanceDialog.cpp + ui/dialogs/CopyInstanceDialog.h + ui/dialogs/CreateShortcutDialog.cpp + ui/dialogs/CreateShortcutDialog.h + ui/dialogs/CustomMessageBox.cpp + ui/dialogs/CustomMessageBox.h + ui/dialogs/ExportInstanceDialog.cpp + ui/dialogs/ExportInstanceDialog.h + ui/dialogs/ExportPackDialog.cpp + ui/dialogs/ExportPackDialog.h + ui/dialogs/ExportToModListDialog.cpp + ui/dialogs/ExportToModListDialog.h + ui/dialogs/IconPickerDialog.cpp + ui/dialogs/IconPickerDialog.h + ui/dialogs/ImportResourceDialog.cpp + ui/dialogs/ImportResourceDialog.h + ui/dialogs/MSALoginDialog.cpp + ui/dialogs/MSALoginDialog.h + ui/dialogs/OfflineLoginDialog.cpp + ui/dialogs/OfflineLoginDialog.h + ui/dialogs/NewComponentDialog.cpp + ui/dialogs/NewComponentDialog.h + ui/dialogs/NewInstanceDialog.cpp + ui/dialogs/NewInstanceDialog.h + ui/dialogs/NewsDialog.cpp + ui/dialogs/NewsDialog.h + ui/dialogs/LauncherHubDialog.cpp + ui/dialogs/LauncherHubDialog.h + ui/pagedialog/PageDialog.cpp + ui/pagedialog/PageDialog.h + ui/dialogs/ProgressDialog.cpp + ui/dialogs/ProgressDialog.h + ui/dialogs/ReviewMessageBox.cpp + ui/dialogs/ReviewMessageBox.h + ui/dialogs/VersionSelectDialog.cpp + ui/dialogs/VersionSelectDialog.h + ui/dialogs/ResourceDownloadDialog.cpp + ui/dialogs/ResourceDownloadDialog.h + ui/dialogs/ScrollMessageBox.cpp + ui/dialogs/ScrollMessageBox.h + ui/dialogs/BlockedModsDialog.cpp + ui/dialogs/BlockedModsDialog.h + ui/dialogs/ChooseProviderDialog.h + ui/dialogs/ChooseProviderDialog.cpp + ui/dialogs/ResourceUpdateDialog.cpp + ui/dialogs/ResourceUpdateDialog.h + ui/dialogs/InstallLoaderDialog.cpp + ui/dialogs/InstallLoaderDialog.h + + # GUI - tasks + ui/tasks/LogUploadTask.cpp + ui/tasks/LogUploadTask.h + + ui/dialogs/skins/SkinManageDialog.cpp + ui/dialogs/skins/SkinManageDialog.h + + ui/dialogs/skins/draw/SkinOpenGLWindow.h + ui/dialogs/skins/draw/SkinOpenGLWindow.cpp + ui/dialogs/skins/draw/Scene.h + ui/dialogs/skins/draw/Scene.cpp + ui/dialogs/skins/draw/BoxGeometry.h + ui/dialogs/skins/draw/BoxGeometry.cpp + + # GUI - widgets + ui/widgets/CheckComboBox.cpp + ui/widgets/CheckComboBox.h + ui/widgets/Common.cpp + ui/widgets/Common.h + ui/widgets/CustomCommands.cpp + ui/widgets/CustomCommands.h + ui/widgets/EnvironmentVariables.cpp + ui/widgets/EnvironmentVariables.h + ui/widgets/IconLabel.cpp + ui/widgets/IconLabel.h + ui/widgets/JavaWizardWidget.cpp + ui/widgets/JavaWizardWidget.h + ui/widgets/LabeledToolButton.cpp + ui/widgets/LabeledToolButton.h + ui/widgets/LanguageSelectionWidget.cpp + ui/widgets/LanguageSelectionWidget.h + ui/widgets/LogView.cpp + ui/widgets/LogView.h + ui/widgets/InfoFrame.cpp + ui/widgets/InfoFrame.h + ui/widgets/ModFilterWidget.cpp + ui/widgets/ModFilterWidget.h + ui/widgets/ModListView.cpp + ui/widgets/ModListView.h + ui/widgets/PageContainer.cpp + ui/widgets/PageContainer.h + ui/widgets/PageContainer_p.h + ui/widgets/ProjectDescriptionPage.h + ui/widgets/ProjectDescriptionPage.cpp + ui/widgets/VariableSizedImageObject.h + ui/widgets/VariableSizedImageObject.cpp + ui/widgets/ProjectItem.h + ui/widgets/ProjectItem.cpp + ui/widgets/SubTaskProgressBar.h + ui/widgets/SubTaskProgressBar.cpp + ui/widgets/VersionListView.cpp + ui/widgets/VersionListView.h + ui/widgets/VersionSelectWidget.cpp + ui/widgets/VersionSelectWidget.h + ui/widgets/ProgressWidget.h + ui/widgets/ProgressWidget.cpp + ui/widgets/WideBar.h + ui/widgets/WideBar.cpp + ui/widgets/AppearanceWidget.h + ui/widgets/AppearanceWidget.cpp + ui/widgets/MinecraftSettingsWidget.h + ui/widgets/MinecraftSettingsWidget.cpp + ui/widgets/JavaSettingsWidget.h + ui/widgets/JavaSettingsWidget.cpp + + # GUI - instance group view + ui/instanceview/InstanceProxyModel.cpp + ui/instanceview/InstanceProxyModel.h + ui/instanceview/AccessibleInstanceView.cpp + ui/instanceview/AccessibleInstanceView.h + ui/instanceview/AccessibleInstanceView_p.h + ui/instanceview/InstanceView.cpp + ui/instanceview/InstanceView.h + ui/instanceview/InstanceDelegate.cpp + ui/instanceview/InstanceDelegate.h + ui/instanceview/VisualGroup.cpp + ui/instanceview/VisualGroup.h +) + +if (APPLE) + set(LAUNCHER_SOURCES + ${LAUNCHER_SOURCES} + ui/themes/ThemeManager.mm + ) +endif() + +if (NOT Apple) + set(LAUNCHER_SOURCES + ${LAUNCHER_SOURCES} + + ui/dialogs/UpdateAvailableDialog.h + ui/dialogs/UpdateAvailableDialog.cpp +) +endif() + +if(WIN32) + set(LAUNCHER_SOURCES + console/WindowsConsole.hpp + console/WindowsConsole.cpp + ${LAUNCHER_SOURCES} + ) +endif() + +qt_wrap_ui(LAUNCHER_UI + ui/MainWindow.ui + ui/setupwizard/PasteWizardPage.ui + ui/setupwizard/AutoJavaWizardPage.ui + ui/setupwizard/LoginWizardPage.ui + ui/pages/global/AccountListPage.ui + ui/pages/global/JavaPage.ui + ui/pages/global/LauncherPage.ui + ui/pages/global/APIPage.ui + ui/pages/global/ProxyPage.ui + ui/pages/global/ExternalToolsPage.ui + ui/pages/instance/ExternalResourcesPage.ui + ui/pages/instance/NotesPage.ui + ui/pages/instance/LogPage.ui + ui/pages/instance/ServersPage.ui + ui/pages/instance/OtherLogsPage.ui + ui/pages/instance/VersionPage.ui + ui/pages/instance/ManagedPackPage.ui + ui/pages/instance/WorldListPage.ui + ui/pages/instance/ScreenshotsPage.ui + ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui + ui/pages/modplatform/atlauncher/AtlPage.ui + ui/pages/modplatform/CustomPage.ui + ui/pages/modplatform/ResourcePage.ui + ui/pages/modplatform/flame/FlamePage.ui + ui/pages/modplatform/legacy_ftb/Page.ui + ui/pages/modplatform/import_ftb/ImportFTBPage.ui + ui/pages/modplatform/ImportPage.ui + ui/pages/modplatform/OptionalModDialog.ui + ui/pages/modplatform/modrinth/ModrinthPage.ui + ui/pages/modplatform/technic/TechnicPage.ui + ui/widgets/CustomCommands.ui + ui/widgets/EnvironmentVariables.ui + ui/widgets/InfoFrame.ui + ui/widgets/ModFilterWidget.ui + ui/widgets/SubTaskProgressBar.ui + ui/widgets/AppearanceWidget.ui + ui/widgets/MinecraftSettingsWidget.ui + ui/widgets/JavaSettingsWidget.ui + ui/dialogs/BackupDialog.ui + ui/dialogs/CopyInstanceDialog.ui + ui/dialogs/CreateShortcutDialog.ui + ui/dialogs/ProfileSetupDialog.ui + ui/dialogs/ProgressDialog.ui + ui/dialogs/NewInstanceDialog.ui + ui/dialogs/NewComponentDialog.ui + ui/dialogs/NewsDialog.ui + ui/dialogs/ProfileSelectDialog.ui + ui/dialogs/ExportInstanceDialog.ui + ui/dialogs/ExportPackDialog.ui + ui/dialogs/ExportToModListDialog.ui + ui/dialogs/IconPickerDialog.ui + ui/dialogs/ImportResourceDialog.ui + ui/dialogs/MSALoginDialog.ui + ui/dialogs/OfflineLoginDialog.ui + ui/dialogs/AboutDialog.ui + ui/dialogs/ReviewMessageBox.ui + ui/dialogs/ScrollMessageBox.ui + ui/dialogs/BlockedModsDialog.ui + ui/dialogs/ChooseProviderDialog.ui + ui/dialogs/skins/SkinManageDialog.ui +) + +qt_wrap_ui(PROJT_UPDATE_UI + ui/dialogs/UpdateAvailableDialog.ui +) + +if (NOT Apple) + set (LAUNCHER_UI ${LAUNCHER_UI} ${PROJT_UPDATE_UI}) +endif() + +qt_add_resources(LAUNCHER_RESOURCES + resources/backgrounds/backgrounds.qrc + resources/multimc/multimc.qrc + resources/pe_dark/pe_dark.qrc + resources/pe_light/pe_light.qrc + resources/pe_colored/pe_colored.qrc + resources/pe_blue/pe_blue.qrc + resources/breeze_dark/breeze_dark.qrc + resources/breeze_light/breeze_light.qrc + resources/OSX/OSX.qrc + resources/iOS/iOS.qrc + resources/flat/flat.qrc + resources/documents/documents.qrc + resources/shaders/shaders.qrc + "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" +) + +qt_wrap_ui(PROJTUPDATER_UI + updater/projtupdater/SelectReleaseDialog.ui + ui/widgets/SubTaskProgressBar.ui + ui/dialogs/ProgressDialog.ui +) + +######## Windows resource files ######## +if(WIN32) + set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) +endif() + +include(CompilerWarnings) + +# Add executable +add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) +set_project_warnings(Launcher_logic + "${Launcher_MSVC_WARNINGS}" + "${Launcher_CLANG_WARNINGS}" + "${Launcher_GCC_WARNINGS}") +target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) +target_link_libraries(Launcher_logic + systeminfo + Launcher_murmur2 + nbt++ + PTlibzippy::PTlibzippy + qdcss + BuildConfig + Qt${QT_VERSION_MAJOR}::Widgets + qrcodegenerator +) +target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus) + +if (UNIX AND NOT CYGWIN AND NOT APPLE) + target_link_libraries(Launcher_logic + gamemode + ) +endif() +target_link_libraries(Launcher_logic + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Xml + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Concurrent + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::NetworkAuth + Qt${QT_VERSION_MAJOR}::OpenGL + ${Launcher_QT_DBUS} + ${Launcher_QT_LIBS} +) +target_link_libraries(Launcher_logic + QuaZip::QuaZip + cmark::cmark + LocalPeer + Launcher_rainbow +) +if (Launcher_QT_DBUS AND TARGET Qt6::DBus) + add_compile_definitions(WITH_QTDBUS) +endif() + +set(LAUNCHER_USE_WEBENGINE ON) +set(LAUNCHER_USE_WEBVIEW2 OFF) +set(LAUNCHER_USE_CEF OFF) +set(LAUNCHER_DISABLE_HUB OFF) +set(_Launcher_CEF_BUILD_FROM_SOURCE_DEFAULT OFF) +if(DEFINED ENV{Launcher_CEF_BUILD_FROM_SOURCE} AND NOT "$ENV{Launcher_CEF_BUILD_FROM_SOURCE}" STREQUAL "") + set(_Launcher_CEF_BUILD_FROM_SOURCE_DEFAULT "$ENV{Launcher_CEF_BUILD_FROM_SOURCE}") +elseif(DEFINED ENV{CEF_BUILD_FROM_SOURCE} AND NOT "$ENV{CEF_BUILD_FROM_SOURCE}" STREQUAL "") + set(_Launcher_CEF_BUILD_FROM_SOURCE_DEFAULT "$ENV{CEF_BUILD_FROM_SOURCE}") +endif() +set(_Launcher_CEF_VERSION_DEFAULT "144.0.11+ge135be2+chromium-144.0.7559.97") +if(DEFINED ENV{Launcher_CEF_VERSION} AND NOT "$ENV{Launcher_CEF_VERSION}" STREQUAL "") + set(_Launcher_CEF_VERSION_DEFAULT "$ENV{Launcher_CEF_VERSION}") +elseif(DEFINED ENV{CEF_VERSION} AND NOT "$ENV{CEF_VERSION}" STREQUAL "") + set(_Launcher_CEF_VERSION_DEFAULT "$ENV{CEF_VERSION}") +endif() +set(_Launcher_CEF_SOURCE_BRANCH_DEFAULT "") +if(_Launcher_CEF_VERSION_DEFAULT MATCHES "chromium-[0-9]+\\.0\\.([0-9]+)\\.[0-9]+") + set(_Launcher_CEF_SOURCE_BRANCH_DEFAULT "${CMAKE_MATCH_1}") +endif() +if(DEFINED ENV{Launcher_CEF_SOURCE_BRANCH} AND NOT "$ENV{Launcher_CEF_SOURCE_BRANCH}" STREQUAL "") + set(_Launcher_CEF_SOURCE_BRANCH_DEFAULT "$ENV{Launcher_CEF_SOURCE_BRANCH}") +elseif(DEFINED ENV{CEF_SOURCE_BRANCH} AND NOT "$ENV{CEF_SOURCE_BRANCH}" STREQUAL "") + set(_Launcher_CEF_SOURCE_BRANCH_DEFAULT "$ENV{CEF_SOURCE_BRANCH}") +endif() +set(_Launcher_CEF_DISTRIBUTION_DEFAULT "minimal") +if(DEFINED ENV{Launcher_CEF_DISTRIBUTION} AND NOT "$ENV{Launcher_CEF_DISTRIBUTION}" STREQUAL "") + set(_Launcher_CEF_DISTRIBUTION_DEFAULT "$ENV{Launcher_CEF_DISTRIBUTION}") +elseif(DEFINED ENV{CEF_DISTRIBUTION} AND NOT "$ENV{CEF_DISTRIBUTION}" STREQUAL "") + set(_Launcher_CEF_DISTRIBUTION_DEFAULT "$ENV{CEF_DISTRIBUTION}") +endif() +set(_Launcher_CEF_DOWNLOAD_URL_DEFAULT "") +if(DEFINED ENV{Launcher_CEF_DOWNLOAD_URL} AND NOT "$ENV{Launcher_CEF_DOWNLOAD_URL}" STREQUAL "") + set(_Launcher_CEF_DOWNLOAD_URL_DEFAULT "$ENV{Launcher_CEF_DOWNLOAD_URL}") +elseif(DEFINED ENV{CEF_DOWNLOAD_URL} AND NOT "$ENV{CEF_DOWNLOAD_URL}" STREQUAL "") + set(_Launcher_CEF_DOWNLOAD_URL_DEFAULT "$ENV{CEF_DOWNLOAD_URL}") +endif() +set(_Launcher_CEF_DOWNLOAD_SHA256_DEFAULT "") +if(DEFINED ENV{Launcher_CEF_DOWNLOAD_SHA256} AND NOT "$ENV{Launcher_CEF_DOWNLOAD_SHA256}" STREQUAL "") + set(_Launcher_CEF_DOWNLOAD_SHA256_DEFAULT "$ENV{Launcher_CEF_DOWNLOAD_SHA256}") +elseif(DEFINED ENV{CEF_DOWNLOAD_SHA256} AND NOT "$ENV{CEF_DOWNLOAD_SHA256}" STREQUAL "") + set(_Launcher_CEF_DOWNLOAD_SHA256_DEFAULT "$ENV{CEF_DOWNLOAD_SHA256}") +endif() +set(_Launcher_CEF_TARGET_ARCH_DEFAULT "") +set(_Launcher_CEF_BINARY_PLATFORM_DEFAULT "") +if(DEFINED ENV{Launcher_CEF_TARGET_ARCH} AND NOT "$ENV{Launcher_CEF_TARGET_ARCH}" STREQUAL "") + set(_Launcher_CEF_TARGET_ARCH_DEFAULT "$ENV{Launcher_CEF_TARGET_ARCH}") +elseif(DEFINED ENV{CEF_TARGET_ARCH} AND NOT "$ENV{CEF_TARGET_ARCH}" STREQUAL "") + set(_Launcher_CEF_TARGET_ARCH_DEFAULT "$ENV{CEF_TARGET_ARCH}") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") + set(_Launcher_CEF_TARGET_ARCH_DEFAULT "x64") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") + set(_Launcher_CEF_TARGET_ARCH_DEFAULT "arm64") +endif() +if(DEFINED ENV{Launcher_CEF_BINARY_PLATFORM} AND NOT "$ENV{Launcher_CEF_BINARY_PLATFORM}" STREQUAL "") + set(_Launcher_CEF_BINARY_PLATFORM_DEFAULT "$ENV{Launcher_CEF_BINARY_PLATFORM}") +elseif(DEFINED ENV{CEF_BINARY_PLATFORM} AND NOT "$ENV{CEF_BINARY_PLATFORM}" STREQUAL "") + set(_Launcher_CEF_BINARY_PLATFORM_DEFAULT "$ENV{CEF_BINARY_PLATFORM}") +elseif(_Launcher_CEF_TARGET_ARCH_DEFAULT STREQUAL "x64") + set(_Launcher_CEF_BINARY_PLATFORM_DEFAULT "linux64") +elseif(_Launcher_CEF_TARGET_ARCH_DEFAULT STREQUAL "arm64") + set(_Launcher_CEF_BINARY_PLATFORM_DEFAULT "linuxarm64") +endif() +set(Launcher_CEF_ROOT "" CACHE PATH "Path to the CEF binary distribution root used for the Linux Hub backend") +option(Launcher_CEF_AUTO_DOWNLOAD "Automatically download Linux CEF when Launcher_CEF_ROOT is not set" ON) +option(Launcher_CEF_BUILD_FROM_SOURCE "Build Linux CEF from source before configuring the launcher" ${_Launcher_CEF_BUILD_FROM_SOURCE_DEFAULT}) +set(Launcher_CEF_VERSION "${_Launcher_CEF_VERSION_DEFAULT}" CACHE STRING "Linux CEF version string used to build the default download URL") +set(Launcher_CEF_SOURCE_BRANCH "${_Launcher_CEF_SOURCE_BRANCH_DEFAULT}" CACHE STRING "CEF source branch used when building Linux CEF from source") +set(Launcher_CEF_DISTRIBUTION "${_Launcher_CEF_DISTRIBUTION_DEFAULT}" CACHE STRING "Linux CEF distribution flavor used when constructing the default download URL") +set(Launcher_CEF_TARGET_ARCH "${_Launcher_CEF_TARGET_ARCH_DEFAULT}" CACHE STRING "CEF target architecture used for Linux source builds") +set(Launcher_CEF_BINARY_PLATFORM "${_Launcher_CEF_BINARY_PLATFORM_DEFAULT}" CACHE STRING "CEF Linux binary platform suffix used when constructing default download URLs") +set(Launcher_CEF_DOWNLOAD_URL "${_Launcher_CEF_DOWNLOAD_URL_DEFAULT}" CACHE STRING "URL to a Linux CEF binary distribution archive") +set(Launcher_CEF_DOWNLOAD_SHA256 "${_Launcher_CEF_DOWNLOAD_SHA256_DEFAULT}" CACHE STRING "Optional SHA256 for Launcher_CEF_DOWNLOAD_URL") +set(Launcher_CEF_DOWNLOAD_DIR "${CMAKE_BINARY_DIR}/downloads/cef" CACHE PATH "Directory used to download and extract the Linux CEF archive") +set(Launcher_CEF_SOURCE_DOWNLOAD_DIR "${CMAKE_BINARY_DIR}/cef-source" CACHE PATH "Directory used for Linux CEF source checkouts and builds") +set(Launcher_CEF_SOURCE_SCRIPT "${CMAKE_SOURCE_DIR}/scripts/build-cef-from-source.sh" CACHE FILEPATH "Script used to build Linux CEF from source") +option(Launcher_CEF_SOURCE_INSTALL_BUILD_DEPS "Run Chromium's Linux build-deps script during CEF source builds" OFF) +if(Launcher_CEF_DOWNLOAD_URL STREQUAL "" AND NOT Launcher_CEF_VERSION STREQUAL "" AND NOT Launcher_CEF_BINARY_PLATFORM STREQUAL "") + set(Launcher_CEF_DOWNLOAD_URL + "https://cef-builds.spotifycdn.com/cef_binary_${Launcher_CEF_VERSION}_${Launcher_CEF_BINARY_PLATFORM}_${Launcher_CEF_DISTRIBUTION}.tar.bz2" + CACHE STRING "URL to a Linux CEF binary distribution archive" FORCE) +elseif(Launcher_CEF_DOWNLOAD_URL STREQUAL "" AND NOT Launcher_CEF_VERSION STREQUAL "") + message(STATUS "No default Linux CEF binary platform mapping for architecture ${CMAKE_SYSTEM_PROCESSOR}; set Launcher_CEF_DOWNLOAD_URL or Launcher_CEF_ROOT explicitly.") +endif() +unset(_Launcher_CEF_BUILD_FROM_SOURCE_DEFAULT) +unset(_Launcher_CEF_VERSION_DEFAULT) +unset(_Launcher_CEF_SOURCE_BRANCH_DEFAULT) +unset(_Launcher_CEF_DISTRIBUTION_DEFAULT) +unset(_Launcher_CEF_TARGET_ARCH_DEFAULT) +unset(_Launcher_CEF_BINARY_PLATFORM_DEFAULT) +unset(_Launcher_CEF_DOWNLOAD_URL_DEFAULT) +unset(_Launcher_CEF_DOWNLOAD_SHA256_DEFAULT) +if(MINGW) + set(LAUNCHER_USE_WEBENGINE OFF) + set(LAUNCHER_DISABLE_HUB ON) +endif() +if(UNIX AND NOT APPLE) + set(LAUNCHER_USE_WEBENGINE OFF) + option(LAUNCHER_USE_CEF "Use CEF backend" ON) +endif() +if(MSVC AND WIN32) + set(LAUNCHER_USE_WEBENGINE OFF) + set(LAUNCHER_USE_WEBVIEW2 ON) +endif() + +if(LAUNCHER_USE_WEBENGINE) + target_compile_definitions(Launcher_logic PUBLIC PROJT_USE_WEBENGINE) + target_link_libraries(Launcher_logic + Qt${QT_VERSION_MAJOR}::WebEngineWidgets + Qt${QT_VERSION_MAJOR}::WebChannel + ) +endif() + +if(LAUNCHER_USE_WEBENGINE AND UNIX AND NOT APPLE AND TARGET MINIZIP::minizip) + target_link_libraries(Launcher_logic MINIZIP::minizip) +endif() + +if(TARGET png_shared) + target_link_libraries(Launcher_logic png_shared) +elseif(TARGET png_static) + target_link_libraries(Launcher_logic png_static) +endif() + +if(LAUNCHER_USE_WEBVIEW2 AND WIN32) + target_compile_definitions(Launcher_logic PUBLIC PROJT_USE_WEBVIEW2) + + set(_WEBVIEW2_SDK_GLOB "${CMAKE_SOURCE_DIR}/dependencies/Microsoft.Web.WebView2.*") + file(GLOB _WEBVIEW2_SDK_DIR LIST_DIRECTORIES true "${_WEBVIEW2_SDK_GLOB}") + list(SORT _WEBVIEW2_SDK_DIR ORDER DESCENDING) + if(_WEBVIEW2_SDK_DIR) + list(GET _WEBVIEW2_SDK_DIR 0 WEBVIEW2_SDK_DIR) + endif() + + find_path(WEBVIEW2_INCLUDE_DIR + NAMES WebView2.h + PATHS + "${WEBVIEW2_SDK_DIR}/build/native/include" + NO_DEFAULT_PATH + ) + set(_WEBVIEW2_ARCH_DIR "") + if(CMAKE_VS_PLATFORM_NAME STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + set(_WEBVIEW2_ARCH_DIR "arm64") + elseif(CMAKE_VS_PLATFORM_NAME MATCHES "64" OR CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_WEBVIEW2_ARCH_DIR "x64") + else() + set(_WEBVIEW2_ARCH_DIR "x86") + endif() + + if(_WEBVIEW2_ARCH_DIR STREQUAL "arm64") + find_library(WEBVIEW2_LOADER_LIB + NAMES WebView2LoaderStatic WebView2Loader + PATHS + "${WEBVIEW2_SDK_DIR}/build/native/arm64" + NO_DEFAULT_PATH + ) + else() + find_library(WEBVIEW2_LOADER_LIB + NAMES WebView2LoaderStatic WebView2Loader + PATHS + "${WEBVIEW2_SDK_DIR}/build/native/${_WEBVIEW2_ARCH_DIR}" + "${WEBVIEW2_SDK_DIR}/build/native/x64" + "${WEBVIEW2_SDK_DIR}/build/native/x86" + NO_DEFAULT_PATH + ) + endif() + + if(NOT WEBVIEW2_INCLUDE_DIR OR NOT WEBVIEW2_LOADER_LIB) + message(FATAL_ERROR "WebView2 SDK not found. Ensure Microsoft.Web.WebView2 is restored into dependencies.") + endif() + + target_include_directories(Launcher_logic PRIVATE "${WEBVIEW2_INCLUDE_DIR}") + target_link_libraries(Launcher_logic "${WEBVIEW2_LOADER_LIB}") +endif() + +if(LAUNCHER_USE_CEF) + target_compile_definitions(Launcher_logic PUBLIC PROJT_USE_CEF) + if(UNIX AND NOT APPLE) + macro(projt_resolve_linux_cef out_var) + set(${out_var} "") + + if(Launcher_CEF_ROOT + AND (NOT EXISTS "${Launcher_CEF_ROOT}/cmake/FindCEF.cmake" + OR NOT EXISTS "${Launcher_CEF_ROOT}/Release/libcef.so")) + message(STATUS "Ignoring invalid Launcher_CEF_ROOT: ${Launcher_CEF_ROOT}") + set(Launcher_CEF_ROOT "" CACHE PATH "Path to the CEF binary distribution root used for the Linux Hub backend" FORCE) + endif() + + if(NOT Launcher_CEF_ROOT AND DEFINED ENV{CEF_ROOT}) + file(TO_CMAKE_PATH "$ENV{CEF_ROOT}" Launcher_CEF_ROOT) + endif() + if(NOT Launcher_CEF_ROOT) + set(_projt_cef_root_candidates + "${CMAKE_SOURCE_DIR}/dependencies/cef" + "${CMAKE_SOURCE_DIR}/third_party/cef" + "${CMAKE_SOURCE_DIR}/vendor/cef" + ) + set(_projt_cef_glob_candidates "") + if(NOT Launcher_CEF_BINARY_PLATFORM STREQUAL "") + file(GLOB _projt_cef_glob_candidates LIST_DIRECTORIES true + "${CMAKE_SOURCE_DIR}/dependencies/cef_binary_*_${Launcher_CEF_BINARY_PLATFORM}*" + "${CMAKE_SOURCE_DIR}/third_party/cef_binary_*_${Launcher_CEF_BINARY_PLATFORM}*" + "${CMAKE_SOURCE_DIR}/vendor/cef_binary_*_${Launcher_CEF_BINARY_PLATFORM}*" + "/tmp/cef-sdk/extracted/cef_binary_*_${Launcher_CEF_BINARY_PLATFORM}*" + ) + endif() + list(APPEND _projt_cef_root_candidates ${_projt_cef_glob_candidates}) + foreach(_projt_cef_candidate ${_projt_cef_root_candidates}) + if(EXISTS "${_projt_cef_candidate}/cmake/FindCEF.cmake" AND EXISTS "${_projt_cef_candidate}/Release/libcef.so") + set(Launcher_CEF_ROOT "${_projt_cef_candidate}") + break() + endif() + endforeach() + unset(_projt_cef_glob_candidates) + unset(_projt_cef_root_candidates) + endif() + + if(NOT Launcher_CEF_ROOT AND Launcher_CEF_BUILD_FROM_SOURCE) + if(NOT EXISTS "${Launcher_CEF_SOURCE_SCRIPT}") + message(FATAL_ERROR "Launcher_CEF_SOURCE_SCRIPT does not exist: ${Launcher_CEF_SOURCE_SCRIPT}") + endif() + if(Launcher_CEF_SOURCE_BRANCH STREQUAL "") + message(FATAL_ERROR "Launcher_CEF_SOURCE_BRANCH is empty. Set it explicitly or provide a Launcher_CEF_VERSION that includes a Chromium branch component.") + endif() + + set(_projt_cef_source_root_file "${CMAKE_BINARY_DIR}/cef-source-root.txt") + file(REMOVE "${_projt_cef_source_root_file}") + message(STATUS "Building Linux CEF from source using branch ${Launcher_CEF_SOURCE_BRANCH}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env + "LAUNCHER_CEF_VERSION=${Launcher_CEF_VERSION}" + "LAUNCHER_CEF_DISTRIBUTION=${Launcher_CEF_DISTRIBUTION}" + "LAUNCHER_CEF_SOURCE_BRANCH=${Launcher_CEF_SOURCE_BRANCH}" + "LAUNCHER_CEF_TARGET_ARCH=${Launcher_CEF_TARGET_ARCH}" + "LAUNCHER_CEF_BINARY_PLATFORM=${Launcher_CEF_BINARY_PLATFORM}" + "LAUNCHER_CEF_SOURCE_DOWNLOAD_DIR=${Launcher_CEF_SOURCE_DOWNLOAD_DIR}" + "LAUNCHER_CEF_SOURCE_OUT_FILE=${_projt_cef_source_root_file}" + "LAUNCHER_CEF_INSTALL_BUILD_DEPS=${Launcher_CEF_SOURCE_INSTALL_BUILD_DEPS}" + /usr/bin/env bash "${Launcher_CEF_SOURCE_SCRIPT}" + RESULT_VARIABLE _projt_cef_source_result + COMMAND_ECHO STDOUT + ) + if(NOT _projt_cef_source_result EQUAL 0) + message(FATAL_ERROR "Linux CEF source build failed with exit code ${_projt_cef_source_result}.") + endif() + if(EXISTS "${_projt_cef_source_root_file}") + file(READ "${_projt_cef_source_root_file}" Launcher_CEF_ROOT) + string(STRIP "${Launcher_CEF_ROOT}" Launcher_CEF_ROOT) + set(Launcher_CEF_ROOT "${Launcher_CEF_ROOT}" CACHE PATH "Path to the CEF binary distribution root used for the Linux Hub backend" FORCE) + endif() + unset(_projt_cef_source_result) + unset(_projt_cef_source_root_file) + endif() + + if(NOT Launcher_CEF_ROOT AND Launcher_CEF_AUTO_DOWNLOAD) + if(Launcher_CEF_DOWNLOAD_URL STREQUAL "") + message(STATUS "Linux CEF auto-download is enabled but Launcher_CEF_DOWNLOAD_URL is empty.") + else() + file(MAKE_DIRECTORY "${Launcher_CEF_DOWNLOAD_DIR}") + get_filename_component(_projt_cef_archive_name "${Launcher_CEF_DOWNLOAD_URL}" NAME) + if(_projt_cef_archive_name STREQUAL "") + set(_projt_cef_archive_name "cef-linux-archive") + endif() + string(MD5 _projt_cef_download_key "${Launcher_CEF_DOWNLOAD_URL}|${Launcher_CEF_DOWNLOAD_SHA256}") + set(_projt_cef_archive_path "${Launcher_CEF_DOWNLOAD_DIR}/${_projt_cef_archive_name}") + set(_projt_cef_extract_dir "${Launcher_CEF_DOWNLOAD_DIR}/extracted-${_projt_cef_download_key}") + set(_projt_cef_extract_stamp "${_projt_cef_extract_dir}/.projt-cef-ready") + + if(EXISTS "${_projt_cef_archive_path}" AND NOT Launcher_CEF_DOWNLOAD_SHA256 STREQUAL "") + file(SHA256 "${_projt_cef_archive_path}" _projt_cef_archive_hash) + if(NOT _projt_cef_archive_hash STREQUAL Launcher_CEF_DOWNLOAD_SHA256) + file(REMOVE "${_projt_cef_archive_path}") + endif() + unset(_projt_cef_archive_hash) + endif() + + if(NOT EXISTS "${_projt_cef_archive_path}") + message(STATUS "Downloading Linux CEF binary distribution from ${Launcher_CEF_DOWNLOAD_URL}") + if(Launcher_CEF_DOWNLOAD_SHA256 STREQUAL "") + file(DOWNLOAD + "${Launcher_CEF_DOWNLOAD_URL}" + "${_projt_cef_archive_path}" + SHOW_PROGRESS + STATUS _projt_cef_download_status + TLS_VERIFY ON + ) + else() + file(DOWNLOAD + "${Launcher_CEF_DOWNLOAD_URL}" + "${_projt_cef_archive_path}" + SHOW_PROGRESS + STATUS _projt_cef_download_status + EXPECTED_HASH "SHA256=${Launcher_CEF_DOWNLOAD_SHA256}" + TLS_VERIFY ON + ) + endif() + list(GET _projt_cef_download_status 0 _projt_cef_download_code) + list(GET _projt_cef_download_status 1 _projt_cef_download_message) + if(NOT _projt_cef_download_code EQUAL 0) + message(FATAL_ERROR "Failed to download Linux CEF from ${Launcher_CEF_DOWNLOAD_URL}: ${_projt_cef_download_message}") + endif() + unset(_projt_cef_download_code) + unset(_projt_cef_download_message) + unset(_projt_cef_download_status) + endif() + + if(NOT EXISTS "${_projt_cef_extract_stamp}") + file(MAKE_DIRECTORY "${_projt_cef_extract_dir}") + message(STATUS "Extracting Linux CEF archive to ${_projt_cef_extract_dir}") + file(ARCHIVE_EXTRACT + INPUT "${_projt_cef_archive_path}" + DESTINATION "${_projt_cef_extract_dir}" + ) + file(WRITE "${_projt_cef_extract_stamp}" "${Launcher_CEF_DOWNLOAD_URL}\n") + endif() + + set(_projt_cef_download_candidates "${_projt_cef_extract_dir}") + if(NOT Launcher_CEF_BINARY_PLATFORM STREQUAL "") + file(GLOB _projt_cef_download_glob LIST_DIRECTORIES true + "${_projt_cef_extract_dir}/cef_binary_*_${Launcher_CEF_BINARY_PLATFORM}*" + "${_projt_cef_extract_dir}/*" + ) + else() + file(GLOB _projt_cef_download_glob LIST_DIRECTORIES true + "${_projt_cef_extract_dir}/*" + ) + endif() + list(APPEND _projt_cef_download_candidates ${_projt_cef_download_glob}) + foreach(_projt_cef_candidate ${_projt_cef_download_candidates}) + if(EXISTS "${_projt_cef_candidate}/cmake/FindCEF.cmake" AND EXISTS "${_projt_cef_candidate}/Release/libcef.so") + set(Launcher_CEF_ROOT "${_projt_cef_candidate}") + set(Launcher_CEF_ROOT "${Launcher_CEF_ROOT}" CACHE PATH "Path to the CEF binary distribution root used for the Linux Hub backend" FORCE) + break() + endif() + endforeach() + unset(_projt_cef_download_glob) + unset(_projt_cef_download_candidates) + unset(_projt_cef_extract_stamp) + unset(_projt_cef_extract_dir) + unset(_projt_cef_archive_path) + unset(_projt_cef_archive_name) + unset(_projt_cef_download_key) + endif() + endif() + + if(Launcher_CEF_ROOT) + set(${out_var} "${Launcher_CEF_ROOT}") + endif() + endmacro() + + projt_resolve_linux_cef(_projt_linux_cef_root) + if(NOT Launcher_CEF_ROOT) + message(FATAL_ERROR "Launcher_CEF_ROOT or CEF_ROOT must point to a Linux CEF binary distribution, or enable auto-download by setting Launcher_CEF_DOWNLOAD_URL.") + endif() + message(STATUS "Using Linux CEF binary distribution at: ${Launcher_CEF_ROOT}") + unset(_projt_linux_cef_root) + unset(projt_resolve_linux_cef) + + list(APPEND CMAKE_MODULE_PATH "${Launcher_CEF_ROOT}/cmake") + set(CEF_ROOT "${Launcher_CEF_ROOT}") + set(CEF_TARGET_ARCH "${Launcher_CEF_TARGET_ARCH}") + set(PROJECT_ARCH "${Launcher_CEF_TARGET_ARCH}") + find_package(CEF REQUIRED) + + if(NOT EXISTS "${CEF_LIB_DEBUG}") + set(CEF_LIB_DEBUG "${CEF_LIB_RELEASE}") + endif() + if(NOT EXISTS "${CEF_BINARY_DIR_DEBUG}/libcef.so") + set(CEF_BINARY_DIR_DEBUG "${CEF_BINARY_DIR_RELEASE}") + endif() + set(_projt_prev_build_shared_libs ${BUILD_SHARED_LIBS}) + set(BUILD_SHARED_LIBS OFF) + if(NOT TARGET libcef_dll_wrapper) + add_subdirectory("${CEF_LIBCEF_DLL_WRAPPER_PATH}" "${CMAKE_CURRENT_BINARY_DIR}/libcef_dll_wrapper") + endif() + set(BUILD_SHARED_LIBS ${_projt_prev_build_shared_libs}) + unset(_projt_prev_build_shared_libs) + if(Launcher_CEF_TARGET_ARCH STREQUAL "arm64") + # Some upstream Linux arm64 CEF packages still inject x86_64-only flags. + foreach(_projt_cef_wrapper_prop COMPILE_OPTIONS INTERFACE_COMPILE_OPTIONS) + get_target_property(_projt_cef_wrapper_value libcef_dll_wrapper ${_projt_cef_wrapper_prop}) + if(_projt_cef_wrapper_value) + list(FILTER _projt_cef_wrapper_value EXCLUDE REGEX "^-m64$") + list(FILTER _projt_cef_wrapper_value EXCLUDE REGEX "^-march=x86-64$") + set_property(TARGET libcef_dll_wrapper PROPERTY ${_projt_cef_wrapper_prop} "${_projt_cef_wrapper_value}") + endif() + unset(_projt_cef_wrapper_value) + endforeach() + get_target_property(_projt_cef_wrapper_compile_flags libcef_dll_wrapper COMPILE_FLAGS) + if(_projt_cef_wrapper_compile_flags) + string(REGEX REPLACE "(^| )-m64( |$)" " " _projt_cef_wrapper_compile_flags "${_projt_cef_wrapper_compile_flags}") + string(REGEX REPLACE "(^| )-march=x86-64( |$)" " " _projt_cef_wrapper_compile_flags "${_projt_cef_wrapper_compile_flags}") + string(STRIP "${_projt_cef_wrapper_compile_flags}" _projt_cef_wrapper_compile_flags) + set_property(TARGET libcef_dll_wrapper PROPERTY COMPILE_FLAGS "${_projt_cef_wrapper_compile_flags}") + endif() + unset(_projt_cef_wrapper_compile_flags) + unset(_projt_cef_wrapper_prop) + endif() + target_compile_options(libcef_dll_wrapper PRIVATE -Wno-c++20-extensions -Wno-error=c++20-extensions) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + # Upstream CEF still triggers pedantic diagnostics under C++17 in GCC/Clang. + target_compile_options(libcef_dll_wrapper PRIVATE -Wno-pedantic -Wno-error=cpp) + # Nix/GCC hardening may inject _FORTIFY_SOURCE even for -O0 debug builds. + target_compile_options(libcef_dll_wrapper PRIVATE + $<$:-U_FORTIFY_SOURCE> + $<$:-D_FORTIFY_SOURCE=0>) + endif() + if(NOT TARGET projt_cef_runtime) + add_library(projt_cef_runtime SHARED IMPORTED GLOBAL) + set_target_properties(projt_cef_runtime PROPERTIES + IMPORTED_LOCATION_DEBUG "${CEF_LIB_DEBUG}" + IMPORTED_LOCATION_RELEASE "${CEF_LIB_RELEASE}" + IMPORTED_LOCATION "${CEF_LIB_RELEASE}") + if(UNIX AND NOT APPLE) + # Nix linkers don't automatically resolve libcef.so transitive DT_NEEDED entries. + find_program(PROJT_CEF_READELF_EXECUTABLE NAMES readelf llvm-readelf) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_projt_cef_expected_elf_class "ELF64") + else() + set(_projt_cef_expected_elf_class "ELF32") + endif() + function(_projt_cef_library_matches_arch out_var candidate) + set(_projt_cef_library_is_compatible TRUE) + if(PROJT_CEF_READELF_EXECUTABLE + AND candidate + AND EXISTS "${candidate}" + AND candidate MATCHES "\\.so(\\..*)?$") + execute_process( + COMMAND "${PROJT_CEF_READELF_EXECUTABLE}" -h "${candidate}" + RESULT_VARIABLE _projt_cef_library_readelf_result + OUTPUT_VARIABLE _projt_cef_library_readelf_output + ERROR_QUIET + ) + if(NOT _projt_cef_library_readelf_result EQUAL 0 + OR NOT _projt_cef_library_readelf_output MATCHES "Class:[ \t]+${_projt_cef_expected_elf_class}") + set(_projt_cef_library_is_compatible FALSE) + endif() + endif() + set(${out_var} "${_projt_cef_library_is_compatible}" PARENT_SCOPE) + endfunction() + set(_projt_cef_runtime_link_libs "") + set(_projt_cef_runtime_library_specs + "glib-2.0;libglib-2.0.so.0" + "gobject-2.0;libgobject-2.0.so.0" + "nspr4;libnspr4.so" + "nss3;libnss3.so" + "nssutil3;libnssutil3.so" + "smime3;libsmime3.so" + "dbus-1;libdbus-1.so.3" + "gio-2.0;libgio-2.0.so.0" + "atk-1.0;libatk-1.0.so.0;libatk-1.0.so" + "atk-bridge-2.0;libatk-bridge-2.0.so.0;libatk-bridge-2.0.so" + "cups;libcups.so.2" + "X11;libX11.so.6" + "Xcomposite;libXcomposite.so.1" + "Xdamage;libXdamage.so.1" + "Xext;libXext.so.6" + "Xfixes;libXfixes.so.3" + "Xrandr;libXrandr.so.2" + "gbm;libgbm.so.1" + "expat;libexpat.so.1" + "xcb;libxcb.so.1" + "xkbcommon;libxkbcommon.so.0" + "cairo;libcairo.so.2;libcairo.so" + "pango-1.0;libpango-1.0.so.0;libpango-1.0.so" + "udev;libudev.so.1" + "asound;libasound.so.2" + "atspi;libatspi.so.0;libatspi.so" + ) + foreach(_projt_cef_runtime_library_spec IN LISTS _projt_cef_runtime_library_specs) + set(_projt_cef_runtime_search_names ${_projt_cef_runtime_library_spec}) + find_library(_projt_cef_runtime_lib_path + NAMES ${_projt_cef_runtime_search_names} + NO_CACHE) + if(_projt_cef_runtime_lib_path) + _projt_cef_library_matches_arch( + _projt_cef_runtime_lib_matches_arch + "${_projt_cef_runtime_lib_path}") + if(NOT _projt_cef_runtime_lib_matches_arch) + unset(_projt_cef_runtime_lib_path) + endif() + unset(_projt_cef_runtime_lib_matches_arch) + endif() + if(NOT _projt_cef_runtime_lib_path) + foreach(_projt_cef_runtime_search_name IN LISTS _projt_cef_runtime_search_names) + if(_projt_cef_runtime_search_name MATCHES "^lib.+\\.so(\\..*)?$") + file(GLOB _projt_cef_runtime_nix_matches + LIST_DIRECTORIES FALSE + "/nix/store/*/lib/${_projt_cef_runtime_search_name}") + if(_projt_cef_runtime_nix_matches) + list(FILTER _projt_cef_runtime_nix_matches EXCLUDE REGEX "/-system-path/") + if(NOT _projt_cef_runtime_nix_matches) + file(GLOB _projt_cef_runtime_nix_matches + LIST_DIRECTORIES FALSE + "/nix/store/*/lib/${_projt_cef_runtime_search_name}") + endif() + list(SORT _projt_cef_runtime_nix_matches) + if(PROJT_CEF_READELF_EXECUTABLE) + foreach(_projt_cef_runtime_nix_match IN LISTS _projt_cef_runtime_nix_matches) + _projt_cef_library_matches_arch( + _projt_cef_runtime_lib_matches_arch + "${_projt_cef_runtime_nix_match}") + if(_projt_cef_runtime_lib_matches_arch) + set(_projt_cef_runtime_lib_path "${_projt_cef_runtime_nix_match}") + break() + endif() + endforeach() + endif() + if(NOT _projt_cef_runtime_lib_path AND NOT PROJT_CEF_READELF_EXECUTABLE) + list(GET _projt_cef_runtime_nix_matches 0 _projt_cef_runtime_lib_path) + endif() + if(_projt_cef_runtime_lib_path) + break() + endif() + endif() + endif() + endforeach() + endif() + if(_projt_cef_runtime_lib_path) + list(APPEND _projt_cef_runtime_link_libs "${_projt_cef_runtime_lib_path}") + endif() + unset(_projt_cef_runtime_lib_path) + unset(_projt_cef_runtime_nix_matches) + unset(_projt_cef_runtime_nix_match) + unset(_projt_cef_runtime_lib_matches_arch) + unset(_projt_cef_runtime_search_name) + unset(_projt_cef_runtime_search_names) + endforeach() + if(_projt_cef_runtime_link_libs) + set_property(TARGET projt_cef_runtime APPEND PROPERTY + INTERFACE_LINK_LIBRARIES "${_projt_cef_runtime_link_libs}") + endif() + if(NOT TARGET projt_cef_runtime_deps) + add_library(projt_cef_runtime_deps INTERFACE) + endif() + if(_projt_cef_runtime_link_libs) + target_link_libraries(projt_cef_runtime_deps INTERFACE ${_projt_cef_runtime_link_libs}) + set(_projt_cef_runtime_rpath_link_options "") + foreach(_projt_cef_runtime_link_lib IN LISTS _projt_cef_runtime_link_libs) + get_filename_component(_projt_cef_runtime_link_dir "${_projt_cef_runtime_link_lib}" DIRECTORY) + list(APPEND _projt_cef_runtime_link_dirs "${_projt_cef_runtime_link_dir}") + endforeach() + list(REMOVE_DUPLICATES _projt_cef_runtime_link_dirs) + foreach(_projt_cef_runtime_link_dir IN LISTS _projt_cef_runtime_link_dirs) + list(APPEND _projt_cef_runtime_rpath_link_options "LINKER:-rpath-link,${_projt_cef_runtime_link_dir}") + endforeach() + if(_projt_cef_runtime_rpath_link_options) + target_link_options(projt_cef_runtime_deps INTERFACE ${_projt_cef_runtime_rpath_link_options}) + endif() + unset(_projt_cef_runtime_rpath_link_options) + unset(_projt_cef_runtime_link_dir) + unset(_projt_cef_runtime_link_dirs) + unset(_projt_cef_runtime_link_lib) + endif() + set(PROJT_CEF_RUNTIME_LINK_LIBS ${_projt_cef_runtime_link_libs}) + unset(_projt_cef_runtime_link_libs) + unset(_projt_cef_runtime_library_specs) + unset(_projt_cef_expected_elf_class) + endif() + endif() + + target_include_directories(Launcher_logic PRIVATE ${CEF_INCLUDE_PATH}) + target_link_libraries(Launcher_logic + libcef_dll_wrapper + projt_cef_runtime + projt_cef_runtime_deps + ${PROJT_CEF_RUNTIME_LINK_LIBS} + ${CEF_STANDARD_LIBS} + ) + if(PROJT_CEF_RUNTIME_LINK_LIBS) + list(REMOVE_DUPLICATES PROJT_CEF_RUNTIME_LINK_LIBS) + # Static libraries don't always re-export raw absolute .so paths in a + # way that reaches final executables on Nix, so publish them explicitly. + set_property(TARGET Launcher_logic APPEND PROPERTY + INTERFACE_LINK_LIBRARIES "${PROJT_CEF_RUNTIME_LINK_LIBS}") + + set(_projt_cef_runtime_link_dirs "") + foreach(_projt_cef_runtime_link_lib IN LISTS PROJT_CEF_RUNTIME_LINK_LIBS) + get_filename_component(_projt_cef_runtime_link_dir "${_projt_cef_runtime_link_lib}" DIRECTORY) + list(APPEND _projt_cef_runtime_link_dirs "${_projt_cef_runtime_link_dir}") + endforeach() + list(REMOVE_DUPLICATES _projt_cef_runtime_link_dirs) + if(_projt_cef_runtime_link_dirs) + set_property(TARGET Launcher_logic APPEND PROPERTY + BUILD_RPATH "${_projt_cef_runtime_link_dirs}") + endif() + unset(_projt_cef_runtime_link_dir) + unset(_projt_cef_runtime_link_lib) + unset(_projt_cef_runtime_link_dirs) + endif() + endif() +endif() + +if(LAUNCHER_DISABLE_HUB) + target_compile_definitions(Launcher_logic PUBLIC PROJT_DISABLE_LAUNCHER_HUB) +endif() + +if(APPLE) + set(CMAKE_MACOSX_RPATH 1) + set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") + + if(Launcher_ENABLE_UPDATER) + file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) + file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) + + find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + add_compile_definitions(SPARKLE_ENABLED) + endif() + + target_link_libraries(Launcher_logic + "-framework AppKit" + "-framework Carbon" + "-framework Foundation" + "-framework ApplicationServices" + ) + if(Launcher_ENABLE_UPDATER) + target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) + endif() +endif() + +target_link_libraries(Launcher_logic) + +add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) +target_link_libraries(${Launcher_Name} Launcher_logic) + +if(LAUNCHER_USE_CEF AND UNIX AND NOT APPLE) + if(CEF_EXE_LINKER_FLAGS) + target_link_options(${Launcher_Name} PRIVATE ${CEF_EXE_LINKER_FLAGS}) + endif() + if(CEF_EXE_LINKER_FLAGS_DEBUG) + target_link_options(${Launcher_Name} PRIVATE $<$:${CEF_EXE_LINKER_FLAGS_DEBUG}>) + endif() + if(CEF_EXE_LINKER_FLAGS_RELEASE) + target_link_options(${Launcher_Name} PRIVATE $<$:${CEF_EXE_LINKER_FLAGS_RELEASE}>) + endif() + + set_target_properties(${Launcher_Name} PROPERTIES + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN") + + foreach(_cef_binary ${CEF_BINARY_FILES}) + add_custom_command( + TARGET ${Launcher_Name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CEF_BINARY_DIR_RELEASE}/${_cef_binary}" + "$/${_cef_binary}" + VERBATIM + ) + + # linuxdeploy scans the installed AppDir, so CEF runtime files must be + # installed next to the launcher binary as well as copied into the build tree. + get_filename_component(_cef_binary_name "${_cef_binary}" NAME) + if(_cef_binary_name STREQUAL "chrome-sandbox") + install(PROGRAMS "${CEF_BINARY_DIR_RELEASE}/${_cef_binary}" + DESTINATION ${BINARY_DEST_DIR} + COMPONENT Runtime) + else() + install(FILES "${CEF_BINARY_DIR_RELEASE}/${_cef_binary}" + DESTINATION ${BINARY_DEST_DIR} + COMPONENT Runtime) + endif() + unset(_cef_binary_name) + endforeach() + foreach(_cef_resource ${CEF_RESOURCE_FILES}) + if(IS_DIRECTORY "${CEF_RESOURCE_DIR}/${_cef_resource}") + add_custom_command( + TARGET ${Launcher_Name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CEF_RESOURCE_DIR}/${_cef_resource}" + "$/${_cef_resource}" + VERBATIM + ) + + install(DIRECTORY "${CEF_RESOURCE_DIR}/${_cef_resource}" + DESTINATION ${BINARY_DEST_DIR} + USE_SOURCE_PERMISSIONS + COMPONENT Runtime) + else() + add_custom_command( + TARGET ${Launcher_Name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CEF_RESOURCE_DIR}/${_cef_resource}" + "$/${_cef_resource}" + VERBATIM + ) + + install(FILES "${CEF_RESOURCE_DIR}/${_cef_resource}" + DESTINATION ${BINARY_DEST_DIR} + COMPONENT Runtime) + endif() + endforeach() +endif() + +if(DEFINED Launcher_AppBinaryName) + set_target_properties(${Launcher_Name} PROPERTIES OUTPUT_NAME "${Launcher_AppBinaryName}") +endif() +if(DEFINED Launcher_BINARY_RPATH) + SET_TARGET_PROPERTIES(${Launcher_Name} PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") +endif() + +if(DEFINED Launcher_APP_BINARY_DEFS) + target_compile_definitions(${Launcher_Name} PRIVATE ${Launcher_APP_BINARY_DEFS}) + target_compile_definitions(Launcher_logic PRIVATE ${Launcher_APP_BINARY_DEFS}) +endif() + +# Install QuaZip shared library DLL on Windows +if(WIN32 AND BUILD_SHARED_LIBS) + if(TARGET QuaZip::QuaZip) + install(FILES $ DESTINATION "." COMPONENT Runtime OPTIONAL) + elseif(TARGET QuaZip) + install(FILES $ DESTINATION "." COMPONENT Runtime OPTIONAL) + endif() +endif() + +# Install QuaZip shared library on macOS into the app bundle Frameworks +if(APPLE) + if(TARGET QuaZip::QuaZip) + install(FILES $ DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime OPTIONAL) + elseif(TARGET QuaZip) + install(FILES $ DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime OPTIONAL) + endif() + install(CODE " + file(GLOB QUAZIP_DYLIBS \"${CMAKE_INSTALL_PREFIX}/${FRAMEWORK_DEST_DIR}/libquazip*.dylib\") + foreach(DYLIB \${QUAZIP_DYLIBS}) + get_filename_component(DYLIB_NAME \"\${DYLIB}\" NAME) + execute_process(COMMAND /usr/bin/install_name_tool -id \"@rpath/\${DYLIB_NAME}\" \"\${DYLIB}\") + endforeach() + ") + + if(TARGET qrencode) + install(FILES $ DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime OPTIONAL) + endif() + install(CODE " + file(GLOB QRENCODE_DYLIBS \"${CMAKE_INSTALL_PREFIX}/${FRAMEWORK_DEST_DIR}/libqrencode*.dylib\") + foreach(DYLIB \${QRENCODE_DYLIBS}) + get_filename_component(DYLIB_NAME \"\${DYLIB}\" NAME) + execute_process(COMMAND /usr/bin/install_name_tool -id \"@rpath/\${DYLIB_NAME}\" \"\${DYLIB}\") + endforeach() + ") + + # Ensure any bundled shared libs installed into ${CMAKE_INSTALL_PREFIX}/lib are available in the app bundle. + install(CODE " + file(MAKE_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/${FRAMEWORK_DEST_DIR}\") + file(GLOB BUNDLED_DYLIBS \"${CMAKE_INSTALL_PREFIX}/lib/*.dylib\") + foreach(DYLIB \${BUNDLED_DYLIBS}) + get_filename_component(DYLIB_NAME \"\${DYLIB}\" NAME) + file(COPY \"\${DYLIB}\" DESTINATION \"${CMAKE_INSTALL_PREFIX}/${FRAMEWORK_DEST_DIR}\") + execute_process(COMMAND /usr/bin/install_name_tool -id \"@rpath/\${DYLIB_NAME}\" \"${CMAKE_INSTALL_PREFIX}/${FRAMEWORK_DEST_DIR}/\${DYLIB_NAME}\") + endforeach() + ") +endif() + +# Install bundled zlib DLLs from bin/ to root on Windows +if(WIN32) + install(CODE " + file(GLOB ZLIB_DLLS \"${CMAKE_INSTALL_PREFIX}/bin/ptlibzippy*.dll\") + foreach(DLL \${ZLIB_DLLS}) + get_filename_component(DLL_NAME \"\${DLL}\" NAME) + message(STATUS \"Copying zlib DLL to root: \${DLL_NAME}\") + file(COPY \"\${DLL}\" DESTINATION \"${CMAKE_INSTALL_PREFIX}\") + endforeach() + ") +endif() + +# Install cmark shared library DLL on Windows +if(WIN32) + if(TARGET cmark::cmark) + get_target_property(CMARK_TYPE cmark::cmark TYPE) + if(CMARK_TYPE STREQUAL "SHARED_LIBRARY") + install(FILES $ DESTINATION "." COMPONENT Runtime OPTIONAL) + endif() + elseif(TARGET cmark) + get_target_property(CMARK_TYPE cmark TYPE) + if(CMARK_TYPE STREQUAL "SHARED_LIBRARY") + install(FILES $ DESTINATION "." COMPONENT Runtime OPTIONAL) + endif() + endif() +endif() + +install(TARGETS ${Launcher_Name} + RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET + BUNDLE DESTINATION "." COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime +) +if(WIN32 AND FORCE_BUNDLED_ZLIB) + if(TARGET ptlibzippy) + install(FILES $ DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime) + endif() +endif() + +# Deploy PDBs +if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + install(FILES $ DESTINATION ${BINARY_DEST_DIR}) +endif() + +if(Launcher_BUILD_UPDATER) + # Updater + add_library(projt_updater_logic STATIC ${PROJTUPDATER_SOURCES} ${TASKS_SOURCES} ${PROJTUPDATER_UI}) + target_include_directories(projt_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(projt_updater_logic + QuaZip::QuaZip + PTlibzippy::PTlibzippy + systeminfo + BuildConfig + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + ${Launcher_QT_DBUS} + ${Launcher_QT_LIBS} + cmark::cmark + ) + + add_executable("${Launcher_Name}_updater" WIN32 updater/projtupdater/updater_main.cpp) + target_sources("${Launcher_Name}_updater" PRIVATE updater/projtupdater/updater.exe.manifest) + target_link_libraries("${Launcher_Name}_updater" projt_updater_logic) + + if(DEFINED Launcher_AppBinaryName) + set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_AppBinaryName}_updater") + endif() + if(DEFINED Launcher_BINARY_RPATH) + SET_TARGET_PROPERTIES("${Launcher_Name}_updater" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") + endif() + + install(TARGETS "${Launcher_Name}_updater" + BUNDLE DESTINATION "." COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime + ) + + # Deploy PDBs + if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + install(FILES $ DESTINATION ${BINARY_DEST_DIR}) + endif() +endif() + +if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) + # File link + add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) + set_project_warnings(filelink_logic + "${Launcher_MSVC_WARNINGS}" + "${Launcher_CLANG_WARNINGS}" + "${Launcher_GCC_WARNINGS}") + + target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(filelink_logic + systeminfo + BuildConfig + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + # Qt${QT_VERSION_MAJOR}::Concurrent + ${Launcher_QT_LIBS} + ) + + add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) + + target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) + # Keep the elevated UAC manifest only for MSVC release artifacts. + if(MSVC) + target_link_options("${Launcher_Name}_filelink" PRIVATE "$<$:/MANIFESTUAC:level='requireAdministrator'>") + endif() + + target_link_libraries("${Launcher_Name}_filelink" filelink_logic) + + if(DEFINED Launcher_AppBinaryName) + set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_AppBinaryName}_filelink") + endif() + if(DEFINED Launcher_BINARY_RPATH) + SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") + endif() + + install(TARGETS "${Launcher_Name}_filelink" + BUNDLE DESTINATION "." COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime + ) + + # Deploy PDBs + if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + install(FILES $ DESTINATION ${BINARY_DEST_DIR}) + endif() +endif() + +if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) + # Add Sparkle updater + # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of + # the framework aren't installed + install(DIRECTORY ${MACOSX_SPARKLE_DIR}/Sparkle.framework DESTINATION ${FRAMEWORK_DEST_DIR} USE_SOURCE_PERMISSIONS) +endif() + +#### The bundle mess! #### +# Bundle utilities are used to complete packages for different platforms - they add all the libraries that would otherwise be missing on the target system. +# NOTE: it seems that this absolutely has to be here, and nowhere else. +if(WIN32 OR (UNIX AND APPLE)) + if(WIN32) + set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") + set(QT_RUNTIME_DIRS "") + set(_qt_target "Qt${QT_VERSION_MAJOR}::Core") + if(TARGET ${_qt_target}) + get_target_property(_qt_debug "${_qt_target}" IMPORTED_LOCATION_DEBUG) + get_target_property(_qt_release "${_qt_target}" IMPORTED_LOCATION_RELEASE) + if(_qt_debug) + get_filename_component(_qt_debug_dir "${_qt_debug}" DIRECTORY) + list(APPEND QT_RUNTIME_DIRS "${_qt_debug_dir}") + endif() + if(_qt_release) + get_filename_component(_qt_release_dir "${_qt_release}" DIRECTORY) + list(APPEND QT_RUNTIME_DIRS "${_qt_release_dir}") + endif() + endif() + if(DEFINED Qt6_DIR) + get_filename_component(_qt6_prefix "${Qt6_DIR}/../../.." ABSOLUTE) + list(APPEND QT_RUNTIME_DIRS "${_qt6_prefix}/bin") + endif() + if(DEFINED Qt5_DIR) + get_filename_component(_qt5_prefix "${Qt5_DIR}/../../.." ABSOLUTE) + list(APPEND QT_RUNTIME_DIRS "${_qt5_prefix}/bin") + endif() + if(DEFINED ENV{QT_ROOT_DIR}) + file(TO_CMAKE_PATH "$ENV{QT_ROOT_DIR}" _qt_root_dir) + if(_qt_root_dir) + list(APPEND QT_RUNTIME_DIRS "${_qt_root_dir}/bin") + endif() + endif() + if(DEFINED ENV{QT_PLUGIN_PATH}) + file(TO_CMAKE_PATH "$ENV{QT_PLUGIN_PATH}" _qt_plugin_path) + if(_qt_plugin_path) + get_filename_component(_qt_plugin_bin "${_qt_plugin_path}/.." ABSOLUTE) + list(APPEND QT_RUNTIME_DIRS "${_qt_plugin_bin}/bin") + endif() + endif() + list(REMOVE_DUPLICATES QT_RUNTIME_DIRS) + elseif(APPLE) + set(QT_RUNTIME_DIRS "") + if(DEFINED Qt6_DIR) + get_filename_component(_qt6_prefix "${Qt6_DIR}/../../.." ABSOLUTE) + set(QT_LIBS_DIR "${_qt6_prefix}/lib") + set(QT_LIBEXECS_DIR "${_qt6_prefix}/libexec") + list(APPEND QT_RUNTIME_DIRS "${QT_LIBS_DIR}" "${QT_LIBEXECS_DIR}") + elseif(DEFINED Qt5_DIR) + get_filename_component(_qt5_prefix "${Qt5_DIR}/../../.." ABSOLUTE) + set(QT_LIBS_DIR "${_qt5_prefix}/lib") + set(QT_LIBEXECS_DIR "${_qt5_prefix}/libexec") + list(APPEND QT_RUNTIME_DIRS "${QT_LIBS_DIR}" "${QT_LIBEXECS_DIR}") + endif() + list(REMOVE_DUPLICATES QT_RUNTIME_DIRS) + endif() + + qt_generate_deploy_script( + TARGET ${Launcher_Name} + OUTPUT_SCRIPT QT_DEPLOY_SCRIPT + CONTENT [=[ + qt_deploy_runtime_dependencies( + EXECUTABLE ${BINARY_DEST_DIR}/$ + BIN_DIR ${BINARY_DEST_DIR} + LIBEXEC_DIR ${LIBRARY_DEST_DIR} + LIB_DIR ${LIBRARY_DEST_DIR} + PLUGINS_DIR ${PLUGIN_DEST_DIR} + NO_OVERWRITE + NO_TRANSLATIONS + NO_COMPILER_RUNTIME + DEPLOY_TOOL_OPTIONS ${QT_DEPLOY_TOOL_OPTIONS} + POST_EXCLUDE_REGEXES "^/opt/homebrew/.*" "^/usr/lib/.*" + ) + ]=] + ) + + # Bundle our linked dependencies + install( + RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET + COMPONENT bundle + DIRECTORIES + ${CMAKE_SYSTEM_LIBRARY_PATH} + ${QT_RUNTIME_DIRS} + ${QT_LIBS_DIR} + ${QT_LIBEXECS_DIR} + ${CMAKE_BINARY_DIR} + ${CMAKE_BINARY_DIR}/$ + ${OPENSSL_ROOT_DIR} + ${OPENSSL_ROOT_DIR}/bin + ${PTLIBZIPPY_ROOT} + ${PTLIBZIPPY_ROOT}/bin + ${PTLIBZIPPY_ROOT_DIR} + ${PTLIBZIPPY_ROOT_DIR}/build/native + ${PTLIBZIPPY_ROOT_DIR}/build/native/bin + PRE_EXCLUDE_REGEXES + "^(api-ms-win|ext-ms)-.*\\.dll$" + "^azure.*\\.dll$" + "^vcruntime.*\\.dll$" + ".*Qt.*\\.dll$" # Exclude Qt from this set, handled by qt_deploy_runtime_dependencies + POST_EXCLUDE_REGEXES + "system32" + "^/opt/homebrew/.*" # Exclude Homebrew paths on macOS + "^/usr/lib/.*" # Exclude system paths + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} + RUNTIME DESTINATION ${BINARY_DEST_DIR} + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} + ) + # Deploy Qt plugins + install( + SCRIPT ${QT_DEPLOY_SCRIPT} + COMPONENT bundle + ) + + # Add qt.conf - this makes Qt stop looking for things outside the bundle + install( + CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" + COMPONENT bundle + ) + # Add qtlogging.ini as a config file + install( + FILES "qtlogging.ini" + DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} + COMPONENT bundle + ) + + # Copy zlib DLLs from bin/ to root on Windows + if(WIN32) + install(CODE " + file(GLOB ZLIB_DLLS \"${CMAKE_INSTALL_PREFIX}/bin/ptlibzippy*.dll\") + foreach(DLL \${ZLIB_DLLS}) + get_filename_component(DLL_NAME \"\${DLL}\" NAME) + message(STATUS \"Copying zlib DLL: \${DLL_NAME} to root\") + file(COPY \"\${DLL}\" DESTINATION \"${CMAKE_INSTALL_PREFIX}\") + endforeach() + ") + endif() +endif() diff --git a/archived/projt-launcher/launcher/CefRuntime.cpp b/archived/projt-launcher/launcher/CefRuntime.cpp new file mode 100644 index 0000000000..6f7f10c0df --- /dev/null +++ b/archived/projt-launcher/launcher/CefRuntime.cpp @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "CefRuntime.h" + +#if defined(PROJT_USE_CEF) && defined(Q_OS_LINUX) + +#include +#include +#include +#include +#include +#include + +#include "include/cef_app.h" +#include "include/wrapper/cef_helpers.h" + +namespace +{ + class LauncherCefApp final : public CefApp + { + public: + void OnBeforeCommandLineProcessing(const CefString&, CefRefPtr command_line) override + { + command_line->AppendSwitch("disable-pinch"); + command_line->AppendSwitch("disable-smooth-scrolling"); + command_line->AppendSwitch("disable-background-networking"); + command_line->AppendSwitch("disable-background-mode"); + command_line->AppendSwitch("disable-component-update"); + command_line->AppendSwitch("disable-sync"); + command_line->AppendSwitch("disable-notifications"); + command_line->AppendSwitch("no-first-run"); + + QString disabledFeatures = QStringLiteral("PlatformNotifications,PushMessaging,NotificationTriggers"); + + const bool enableGpu = qEnvironmentVariableIntValue("PROJT_CEF_ENABLE_GPU") == 1 + || qEnvironmentVariableIntValue("LAUNCHER_CEF_ENABLE_GPU") == 1; + if (!enableGpu) + { + command_line->AppendSwitch("disable-gpu"); + command_line->AppendSwitch("disable-gpu-compositing"); + command_line->AppendSwitch("disable-gpu-vsync"); + disabledFeatures += QStringLiteral(",VaapiVideoDecoder,Vulkan"); + } + + command_line->AppendSwitchWithValue("disable-features", disabledFeatures.toStdString()); + } + + IMPLEMENT_REFCOUNTING(LauncherCefApp); + }; +} + +#endif + +namespace projt::cef +{ + Runtime::Runtime(QObject* parent) : QObject(parent) + {} + + Runtime& Runtime::instance() + { + static Runtime runtime; + return runtime; + } + + int Runtime::executeSecondaryProcess(int argc, char* argv[]) + { +#if defined(PROJT_USE_CEF) && defined(Q_OS_LINUX) + CefMainArgs mainArgs(argc, argv); + return CefExecuteProcess(mainArgs, new LauncherCefApp(), nullptr); +#else + Q_UNUSED(argc); + Q_UNUSED(argv); + return -1; +#endif + } + + bool Runtime::initializeBrowserProcess(int argc, char* argv[]) + { +#if defined(PROJT_USE_CEF) && defined(Q_OS_LINUX) + if (m_initialized) + { + return true; + } + + CefMainArgs mainArgs(argc, argv); + CefSettings settings; + settings.no_sandbox = true; + settings.multi_threaded_message_loop = false; + settings.external_message_pump = false; + settings.windowless_rendering_enabled = true; + + const QString executablePath = QCoreApplication::applicationFilePath(); + const QString executableDir = QFileInfo(executablePath).absolutePath(); + const QString dataRoot = + QDir::cleanPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/cef"); + QDir().mkpath(dataRoot); + + CefString(&settings.browser_subprocess_path) = executablePath.toStdString(); + CefString(&settings.resources_dir_path) = executableDir.toStdString(); + CefString(&settings.locales_dir_path) = QDir(executableDir).filePath("locales").toStdString(); + CefString(&settings.root_cache_path) = dataRoot.toStdString(); + CefString(&settings.cache_path) = QDir(dataRoot).filePath("cache").toStdString(); + CefString(&settings.user_agent_product) = "ProjTLauncher"; + + const bool ok = CefInitialize(mainArgs, settings, new LauncherCefApp(), nullptr); + m_exitCode = ok ? 0 : CefGetExitCode(); + if (!ok) + { + if (m_exitCode == 0) + { + m_exitCode = 1; + } + return false; + } + + auto* timer = new QTimer(this); + timer->setInterval(10); + timer->setTimerType(Qt::PreciseTimer); + connect(timer, &QTimer::timeout, this, []() { CefDoMessageLoopWork(); }); + timer->start(); + + m_initialized = true; + return true; +#else + Q_UNUSED(argc); + Q_UNUSED(argv); + return false; +#endif + } + + void Runtime::shutdown() + { +#if defined(PROJT_USE_CEF) && defined(Q_OS_LINUX) + if (!m_initialized) + { + return; + } + + for (auto* timer : findChildren()) + { + timer->stop(); + timer->deleteLater(); + } + CefShutdown(); + m_initialized = false; +#endif + } + + bool Runtime::isInitialized() const + { + return m_initialized; + } + + int Runtime::exitCode() const + { + return m_exitCode; + } +} diff --git a/archived/projt-launcher/launcher/CefRuntime.h b/archived/projt-launcher/launcher/CefRuntime.h new file mode 100644 index 0000000000..76a2895bfb --- /dev/null +++ b/archived/projt-launcher/launcher/CefRuntime.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +namespace projt::cef +{ + class Runtime : public QObject + { + Q_OBJECT + + public: + static Runtime& instance(); + static int executeSecondaryProcess(int argc, char* argv[]); + + bool initializeBrowserProcess(int argc, char* argv[]); + void shutdown(); + + bool isInitialized() const; + int exitCode() const; + + private: + explicit Runtime(QObject* parent = nullptr); + + bool m_initialized = false; + int m_exitCode = 0; + }; +} diff --git a/archived/projt-launcher/launcher/Commandline.cpp b/archived/projt-launcher/launcher/Commandline.cpp new file mode 100644 index 0000000000..a710588d13 --- /dev/null +++ b/archived/projt-launcher/launcher/Commandline.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +======================================================================== */ + +#include "Commandline.h" + +/** + * @file libutil/src/cmdutils.cpp + */ + +namespace Commandline +{ + + // commandline splitter + QStringList splitArgs(QString args) + { + QStringList argv; + QString current; + bool escape = false; + QChar inquotes; + for (int i = 0; i < args.length(); i++) + { + QChar cchar = args.at(i); + + // \ escaped + if (escape) + { + current += cchar; + escape = false; + // in "quotes" + } + else if (!inquotes.isNull()) + { + if (cchar == '\\') + escape = true; + else if (cchar == inquotes) + inquotes = QChar::Null; + else + current += cchar; + // otherwise + } + else + { + if (cchar == ' ') + { + if (!current.isEmpty()) + { + argv << current; + current.clear(); + } + } + else if (cchar == '"' || cchar == '\'') + inquotes = cchar; + else + current += cchar; + } + } + if (!current.isEmpty()) + argv << current; + return argv; + } +} // namespace Commandline diff --git a/archived/projt-launcher/launcher/Commandline.h b/archived/projt-launcher/launcher/Commandline.h new file mode 100644 index 0000000000..8babdea185 --- /dev/null +++ b/archived/projt-launcher/launcher/Commandline.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * === Upstream License Block (Do Not Modify) ============================== + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include +#include + +/** + * @file libutil/include/cmdutils.h + * @brief commandline parsing and processing utilities + */ + +namespace Commandline +{ + + /** + * @brief split a string into argv items like a shell would do + * @param args the argument string + * @return a QStringList containing all arguments + */ + QStringList splitArgs(QString args); +} // namespace Commandline diff --git a/archived/projt-launcher/launcher/DataMigrationTask.cpp b/archived/projt-launcher/launcher/DataMigrationTask.cpp new file mode 100644 index 0000000000..c04fa4e582 --- /dev/null +++ b/archived/projt-launcher/launcher/DataMigrationTask.cpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + + +======================================================================== */ + +#include "DataMigrationTask.h" + +#include "FileSystem.h" + +#include +#include +#include + +#include + +DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathMatcher) + : Task(), + m_sourcePath(sourcePath), + m_targetPath(targetPath), + m_pathMatcher(pathMatcher), + m_copy(sourcePath, targetPath) +{ + m_copy.matcher(m_pathMatcher).whitelist(true); +} + +void DataMigrationTask::executeTask() +{ + setStatus(tr("Scanning files...")); + + // 1. Scan + // Check how many files we gotta copy + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), + [this] + { + return m_copy(true); // dry run to collect amount of files + }); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); +} + +void DataMigrationTask::dryRunFinished() +{ + disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); + disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); + + if (!m_copyFuture.isValid() || !m_copyFuture.result()) + { + emitFailed(tr("Failed to scan source path.")); + return; + } + + // 2. Copy + // Actually copy all files now. + m_toCopy = m_copy.totalCopied(); + connect(&m_copy, + &FS::copy::fileCopied, + [&, this](const QString& relativeName) + { + QString shortenedName = relativeName; + // shorten the filename to hopefully fit into one line + if (shortenedName.length() > 50) + shortenedName = relativeName.left(20) + "…" + relativeName.right(29); + setProgress(m_copy.totalCopied(), m_toCopy); + setStatus(tr("Copying %1…").arg(shortenedName)); + }); + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), + [this] + { + return m_copy(false); // actually copy now + }); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); +} + +void DataMigrationTask::dryRunAborted() +{ + emitAborted(); +} + +void DataMigrationTask::copyFinished() +{ + disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); + disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); + + if (!m_copyFuture.isValid() || !m_copyFuture.result()) + { + emitFailed(tr("Some paths could not be copied!")); + return; + } + + emitSucceeded(); +} + +void DataMigrationTask::copyAborted() +{ + emitAborted(); +} diff --git a/archived/projt-launcher/launcher/DataMigrationTask.h b/archived/projt-launcher/launcher/DataMigrationTask.h new file mode 100644 index 0000000000..cf111cab65 --- /dev/null +++ b/archived/projt-launcher/launcher/DataMigrationTask.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + + +======================================================================== */ + +#pragma once + +#include "FileSystem.h" +#include "Filter.h" +#include "tasks/Task.h" + +#include +#include + +/* + * Migrate existing data from other MMC-like launchers. + */ + +class DataMigrationTask : public Task +{ + Q_OBJECT + public: + explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathmatcher); + ~DataMigrationTask() override = default; + + protected: + virtual void executeTask() override; + + protected slots: + void dryRunFinished(); + void dryRunAborted(); + void copyFinished(); + void copyAborted(); + + private: + const QString& m_sourcePath; + const QString& m_targetPath; + const Filter m_pathMatcher; + + FS::copy m_copy; + int m_toCopy = 0; + QFuture m_copyFuture; + QFutureWatcher m_copyFutureWatcher; +}; diff --git a/archived/projt-launcher/launcher/DefaultVariable.h b/archived/projt-launcher/launcher/DefaultVariable.h new file mode 100644 index 0000000000..cd8b6919e8 --- /dev/null +++ b/archived/projt-launcher/launcher/DefaultVariable.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +template +class DefaultVariable +{ + public: + DefaultVariable(const T& value) + { + defaultValue = value; + } + DefaultVariable& operator=(const T& value) + { + currentValue = value; + is_default = currentValue == defaultValue; + is_explicit = true; + return *this; + } + operator const T&() const + { + return is_default ? defaultValue : currentValue; + } + bool isDefault() const + { + return is_default; + } + bool isExplicit() const + { + return is_explicit; + } + + private: + T currentValue; + T defaultValue; + bool is_default = true; + bool is_explicit = false; +}; diff --git a/archived/projt-launcher/launcher/DesktopServices.cpp b/archived/projt-launcher/launcher/DesktopServices.cpp new file mode 100644 index 0000000000..5b9e9f8e40 --- /dev/null +++ b/archived/projt-launcher/launcher/DesktopServices.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 dada513 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2022 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ======================================================================== */ +#include "DesktopServices.h" +#include +#include +#include +#include +#include "FileSystem.h" + +namespace DesktopServices +{ + bool openPath(const QFileInfo& path, bool ensureFolderPathExists) + { + qDebug() << "Opening path" << path; + if (ensureFolderPathExists) + { + FS::ensureFolderPathExists(path); + } + return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath())); + } + + bool openPath(const QString& path, bool ensureFolderPathExists) + { + return openPath(QFileInfo(path), ensureFolderPathExists); + } + + bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid) + { + qDebug() << "Running" << application << "with args" << args.join(' '); + return QProcess::startDetached(application, args, workingDirectory, pid); + } + + bool openUrl(const QUrl& url) + { + qDebug() << "Opening URL" << url.toString(); + return QDesktopServices::openUrl(url); + } + + bool isFlatpak() + { +#ifdef Q_OS_LINUX + return QFile::exists("/.flatpak-info"); +#else + return false; +#endif + } + + bool isSnap() + { +#ifdef Q_OS_LINUX + return getenv("SNAP"); +#else + return false; +#endif + } + +} // namespace DesktopServices diff --git a/archived/projt-launcher/launcher/DesktopServices.h b/archived/projt-launcher/launcher/DesktopServices.h new file mode 100644 index 0000000000..6aee0d0828 --- /dev/null +++ b/archived/projt-launcher/launcher/DesktopServices.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +class QFileInfo; + +/** + * This wraps around QDesktopServices and adds workarounds where needed + * Use this instead of QDesktopServices! + */ +namespace DesktopServices +{ + /** + * Open a path in whatever application is applicable. + * @param ensureFolderPathExists Make sure the path exists + */ + bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false); + + /** + * Open a path in whatever application is applicable. + * @param ensureFolderPathExists Make sure the path exists + */ + bool openPath(const QString& path, bool ensureFolderPathExists = false); + + /** + * Run an application + */ + bool run(const QString& application, + const QStringList& args, + const QString& workingDirectory = QString(), + qint64* pid = 0); + + /** + * Open the URL, most likely in a browser. Maybe. + */ + bool openUrl(const QUrl& url); + + /** + * Determine whether the launcher is running in a Flatpak environment + */ + bool isFlatpak(); + + /** + * Determine whether the launcher is running in a Snap environment + */ + bool isSnap(); +} // namespace DesktopServices diff --git a/archived/projt-launcher/launcher/Exception.h b/archived/projt-launcher/launcher/Exception.h new file mode 100644 index 0000000000..d6e3207208 --- /dev/null +++ b/archived/projt-launcher/launcher/Exception.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +class Exception : public std::exception +{ + public: + Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) + { + qCritical() << "Exception:" << message; + } + Exception(const Exception& other) : std::exception(), m_message(other.m_message) + {} + virtual ~Exception() noexcept + {} + const char* what() const noexcept + { + return m_message.constData(); + } + QString cause() const + { + return QString::fromUtf8(m_message); + } + + private: + QByteArray m_message; +}; diff --git a/archived/projt-launcher/launcher/ExponentialSeries.h b/archived/projt-launcher/launcher/ExponentialSeries.h new file mode 100644 index 0000000000..9b0bbea395 --- /dev/null +++ b/archived/projt-launcher/launcher/ExponentialSeries.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +template +inline void clamp(T& current, T min, T max) +{ + if (current < min) + { + current = min; + } + else if (current > max) + { + current = max; + } +} + +// List of numbers from min to max. Next is exponent times bigger than previous. + +class ExponentialSeries +{ + public: + ExponentialSeries(unsigned min, unsigned max, unsigned exponent = 2) + { + m_current = m_min = min; + m_max = max; + m_exponent = exponent; + } + void reset() + { + m_current = m_min; + } + unsigned operator()() + { + unsigned retval = m_current; + m_current *= m_exponent; + clamp(m_current, m_min, m_max); + return retval; + } + unsigned m_current; + unsigned m_min; + unsigned m_max; + unsigned m_exponent; +}; diff --git a/archived/projt-launcher/launcher/FileSystem.cpp b/archived/projt-launcher/launcher/FileSystem.cpp new file mode 100644 index 0000000000..54712b37a6 --- /dev/null +++ b/archived/projt-launcher/launcher/FileSystem.cpp @@ -0,0 +1,2002 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ======================================================================== */ + +#include "FileSystem.h" +#include + +#include "BuildConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DesktopServices.h" +#include "PSaveFile.h" +#include "StringUtils.h" + +#if defined Q_OS_WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// for ShellExecute +#include +#include +#include +#else +#include +#endif + +#if defined(Q_OS_LINUX) +#include +#include +#include +#include +#endif + +#include +namespace fs = std::filesystem; + +// clone +#if defined(Q_OS_LINUX) +#include +#include /* Definition of FICLONE* constants */ +#include +#include +#include +#elif defined(Q_OS_MACOS) +#include +#include +#elif defined(Q_OS_WIN) +// winbtrfs clone vs rundll32 shellbtrfs.dll,ReflinkCopy +#include +#include +#include +#include +// refs +#include +#if defined(__MINGW32__) +#include +#endif +#endif + +#if defined(Q_OS_WIN) + +#if defined(__MINGW32__) + +// Avoid re-defining structs retroactively added to MinGW +// https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729 +#if __MINGW64_VERSION_MAJOR < 13 + +struct _DUPLICATE_EXTENTS_DATA +{ + HANDLE FileHandle; + LARGE_INTEGER SourceFileOffset; + LARGE_INTEGER TargetFileOffset; + LARGE_INTEGER ByteCount; +}; + +using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; +using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; +#endif + +struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER +{ + WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 + WORD Reserved; // Must be 0 + DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx + DWORD ChecksumChunkSizeInBytes; + DWORD ClusterSizeInBytes; +}; + +using FSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER; +using PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER*; + +struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER +{ + WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 + WORD Reserved; // Must be 0 + DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx +}; + +using FSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER; +using PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER*; + +#endif + +#ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE +#define FSCTL_DUPLICATE_EXTENTS_TO_FILE CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA) +#endif + +#ifndef FSCTL_GET_INTEGRITY_INFORMATION +#define FSCTL_GET_INTEGRITY_INFORMATION \ + CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 159, METHOD_BUFFERED, FILE_ANY_ACCESS) // FSCTL_GET_INTEGRITY_INFORMATION_BUFFER +#endif + +#ifndef FSCTL_SET_INTEGRITY_INFORMATION +#define FSCTL_SET_INTEGRITY_INFORMATION \ + CTL_CODE(FILE_DEVICE_FILE_SYSTEM, \ + 160, \ + METHOD_BUFFERED, \ + FILE_READ_DATA | FILE_WRITE_DATA) // FSCTL_SET_INTEGRITY_INFORMATION_BUFFER +#endif + +#ifndef ERROR_NOT_CAPABLE +#define ERROR_NOT_CAPABLE 775L +#endif + +#ifndef ERROR_BLOCK_TOO_MANY_REFERENCES +#define ERROR_BLOCK_TOO_MANY_REFERENCES 347L +#endif + +#endif + +namespace FS +{ + + void ensureExists(const QDir& dir) + { + if (!QDir().mkpath(dir.absolutePath())) + { + throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")"); + } + } + + void write(const QString& filename, const QByteArray& data) + { + ensureExists(QFileInfo(filename).dir()); + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) + { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); + } + if (data.size() != file.write(data)) + { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); + } + if (!file.commit()) + { + throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); + } + } + + void appendSafe(const QString& filename, const QByteArray& data) + { + ensureExists(QFileInfo(filename).dir()); + QByteArray buffer; + try + { + buffer = read(filename); + } + catch (FileSystemException&) + { + buffer = QByteArray(); + } + buffer.append(data); + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) + { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); + } + if (buffer.size() != file.write(buffer)) + { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); + } + if (!file.commit()) + { + throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); + } + } + + void append(const QString& filename, const QByteArray& data) + { + ensureExists(QFileInfo(filename).dir()); + QFile file(filename); + if (!file.open(QFile::Append)) + { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); + } + if (data.size() != file.write(data)) + { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); + } + } + + QByteArray read(const QString& filename) + { + QFile file(filename); + if (!file.open(QFile::ReadOnly)) + { + throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString()); + } + const qint64 size = file.size(); + QByteArray data(int(size), 0); + const qint64 ret = file.read(data.data(), size); + if (ret == -1 || ret != size) + { + throw FileSystemException("Error reading data from " + filename + ": " + file.errorString()); + } + return data; + } + + bool updateTimestamp(const QString& filename) + { +#ifdef Q_OS_WIN32 + std::wstring filename_utf_16 = filename.toStdWString(); + return (_wutime64(filename_utf_16.c_str(), nullptr) == 0); +#else + QByteArray filenameBA = QFile::encodeName(filename); + return (utime(filenameBA.data(), nullptr) == 0); +#endif + } + + bool ensureFilePathExists(QString filenamepath) + { + QFileInfo a(filenamepath); + QDir dir; + QString ensuredPath = a.path(); + bool success = dir.mkpath(ensuredPath); + return success; + } + + bool ensureFolderPathExists(const QFileInfo folderPath) + { + QDir dir; + QString ensuredPath = folderPath.filePath(); + if (folderPath.exists()) + return true; + + bool success = dir.mkpath(ensuredPath); + return success; + } + + bool ensureFolderPathExists(const QString folderPathName) + { + return ensureFolderPathExists(QFileInfo(folderPathName)); + } + + bool copyFileAttributes(QString src, QString dst) + { +#ifdef Q_OS_WIN32 + auto attrs = GetFileAttributesW(src.toStdWString().c_str()); + if (attrs == INVALID_FILE_ATTRIBUTES) + return false; + return SetFileAttributesW(dst.toStdWString().c_str(), attrs); +#else + Q_UNUSED(src); + Q_UNUSED(dst); +#endif + return true; +} + + // needs folders to exists + void copyFolderAttributes(QString src, QString dst, QString relative) + { + auto path = PathCombine(src, relative); + QDir dsrc(src); + while ((path = QFileInfo(path).path()).length() >= src.length()) + { + auto dst_path = PathCombine(dst, dsrc.relativeFilePath(path)); + copyFileAttributes(path, dst_path); + } + } + + /** + * @brief Copies a directory and it's contents from src to dest + * @param offset subdirectory form src to copy to dest + * @return if there was an error during the filecopy + */ + bool copy::operator()(const QString& offset, bool dryRun) + { + using copy_opts = fs::copy_options; + m_copied = 0; // reset counter + m_failedPaths.clear(); + +// NOTE always deep copy on windows. the alternatives are too messy. +#if defined Q_OS_WIN32 + m_followSymlinks = true; +#endif + + auto src = PathCombine(m_src.absolutePath(), offset); + auto dst = PathCombine(m_dst.absolutePath(), offset); + + std::error_code err; + + fs::copy_options opt = copy_opts::none; + + // The default behavior is to follow symlinks + if (!m_followSymlinks) + opt |= copy_opts::copy_symlinks; + + if (m_overwrite) + opt |= copy_opts::overwrite_existing; + + // Function that'll do the actual copying + auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) + { + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) + return; + + auto dst_path = PathCombine(dst, relative_dst_path); + if (!dryRun) + { + ensureFilePathExists(dst_path); +#ifdef Q_OS_WIN32 + copyFolderAttributes(src, dst, relative_dst_path); +#endif + fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); + } + if (err) + { + qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); + qDebug() << "Source file:" << src_path; + qDebug() << "Destination file:" << dst_path; + m_failedPaths.append(dst_path); + emit copyFailed(relative_dst_path); + return; + } + m_copied++; + emit fileCopied(relative_dst_path); + }; + + // We can't use copy_opts::recursive because we need to take into account the + // blacklisted paths, so we iterate over the source directory, and if there's no blacklist + // match, we copy the file. + QDir src_dir(src); + QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); + + while (source_it.hasNext()) + { + auto src_path = source_it.next(); + auto relative_path = src_dir.relativeFilePath(src_path); + + copy_file(src_path, relative_path); + } + + // If the root src is not a directory, the previous iterator won't run. + if (!fs::is_directory(StringUtils::toStdString(src))) + copy_file(src, ""); + + return err.value() == 0; + } + + /// qDebug print support for the LinkPair struct + QDebug operator<<(QDebug debug, const LinkPair& lp) + { + QDebugStateSaver saver(debug); + + debug.nospace() << "LinkPair{ src: " << lp.src << " , dst: " << lp.dst << " }"; + return debug; + } + + bool create_link::operator()(const QString& offset, bool dryRun) + { + m_linked = 0; // reset counter + m_path_results.clear(); + m_links_to_make.clear(); + + m_path_results.clear(); + + make_link_list(offset); + + if (!dryRun) + return make_links(); + + return true; + } + + /** + * @brief Make a list of all the links to make + * @param offset subdirectory of src to link to dest + */ + void create_link::make_link_list(const QString& offset) + { + for (auto pair : m_path_pairs) + { + const QString& srcPath = pair.src; + const QString& dstPath = pair.dst; + + auto src = PathCombine(QDir(srcPath).absolutePath(), offset); + auto dst = PathCombine(QDir(dstPath).absolutePath(), offset); + + // you can't hard link a directory so make sure if we deal with a directory we do so recursively + if (m_useHardLinks) + m_recursive = true; + + // Function that'll do the actual linking + auto link_file = [this, dst](QString src_path, QString relative_dst_path) + { + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) + { + qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; + return; + } + + auto dst_path = PathCombine(dst, relative_dst_path); + LinkPair link = { src_path, dst_path }; + m_links_to_make.append(link); + }; + + if ((!m_recursive) || !fs::is_directory(StringUtils::toStdString(src))) + { + if (m_debug) + qDebug() << "linking single file or dir:" << src << "to" << dst; + link_file(src, ""); + } + else + { + if (m_debug) + qDebug() << "linking recursively:" << src << "to" << dst << ", max_depth:" << m_max_depth; + QDir src_dir(src); + QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); + + QStringList linkedPaths; + + while (source_it.hasNext()) + { + auto src_path = source_it.next(); + auto relative_path = src_dir.relativeFilePath(src_path); + + if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth) + { + relative_path = pathTruncate(relative_path, m_max_depth); + src_path = src_dir.filePath(relative_path); + if (linkedPaths.contains(src_path)) + { + continue; + } + } + + linkedPaths.append(src_path); + + link_file(src_path, relative_path); + } + } + } + } + + bool create_link::make_links() + { + for (auto link : m_links_to_make) + { + QString src_path = link.src; + QString dst_path = link.dst; + auto src_path_std = StringUtils::toStdString(link.src); + auto dst_path_std = StringUtils::toStdString(link.dst); + + ensureFilePathExists(dst_path); + if (m_useHardLinks) + { + if (m_debug) + qDebug() << "making hard link:" << src_path << "to" << dst_path; + fs::create_hard_link(src_path_std, dst_path_std, m_os_err); + } + else if (fs::is_directory(src_path_std)) + { + if (m_debug) + qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; + fs::create_directory_symlink(src_path_std, dst_path_std, m_os_err); + } + else + { + if (m_debug) + qDebug() << "making symlink:" << src_path << "to" << dst_path; + fs::create_symlink(src_path_std, dst_path_std, m_os_err); + } + + if (m_os_err) + { + qWarning() << "Failed to link files:" << QString::fromStdString(m_os_err.message()); + qDebug() << "Source file:" << src_path; + qDebug() << "Destination file:" << dst_path; + qDebug() << "Error category:" << m_os_err.category().name(); + qDebug() << "Error code:" << m_os_err.value(); + emit linkFailed(src_path, dst_path, QString::fromStdString(m_os_err.message()), m_os_err.value()); + } + else + { + m_linked++; + emit fileLinked(src_path, dst_path); + } + if (m_os_err) + return false; + } + return true; + } + + void create_link::runPrivileged(const QString& offset) + { + m_linked = 0; // reset counter + m_path_results.clear(); + m_links_to_make.clear(); + + bool gotResults = false; + + make_link_list(offset); + + QString serverName = + BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); + + connect( + &m_linkServer, + &QLocalServer::newConnection, + this, + [this, &gotResults]() + { + qDebug() << "Client connected, sending out pairs"; + // construct block of data to send + QByteArray block; + QDataStream out(&block, QIODevice::WriteOnly); + + qint32 blocksize = quint32(sizeof(quint32)); + for (auto link : m_links_to_make) + { + blocksize += quint32(link.src.size()); + blocksize += quint32(link.dst.size()); + } + qDebug() << "About to write block of size:" << blocksize; + out << blocksize; + + out << quint32(m_links_to_make.length()); + for (auto link : m_links_to_make) + { + out << link.src; + out << link.dst; + } + + QLocalSocket* clientConnection = m_linkServer.nextPendingConnection(); + connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater); + + connect(clientConnection, + &QLocalSocket::readyRead, + this, + [&, clientConnection]() + { + QDataStream in; + quint32 blockSize = 0; + in.setDevice(clientConnection); + + qDebug() << "Reading path results from client"; + qDebug() << "bytes available" << clientConnection->bytesAvailable(); + + // Relies on the fact that QDataStream serializes a quint32 into + // sizeof(quint32) bytes + if (clientConnection->bytesAvailable() < (int)sizeof(quint32)) + return; + qDebug() << "reading block size"; + in >> blockSize; + + qDebug() << "blocksize is" << blockSize; + qDebug() << "bytes available" << clientConnection->bytesAvailable(); + if (clientConnection->bytesAvailable() < blockSize || in.atEnd()) + return; + + quint32 numResults; + in >> numResults; + qDebug() << "numResults" << numResults; + + for (quint32 i = 0; i < numResults; i++) + { + FS::LinkResult result; + in >> result.src; + in >> result.dst; + in >> result.err_msg; + qint32 err_value; + in >> err_value; + result.err_value = err_value; + if (result.err_value) + { + qDebug() << "privileged link fail" << result.src << "to" << result.dst << "code" + << result.err_value << result.err_msg; + emit linkFailed(result.src, result.dst, result.err_msg, result.err_value); + } + else + { + qDebug() << "privileged link success" << result.src << "to" << result.dst; + m_linked++; + emit fileLinked(result.src, result.dst); + } + m_path_results.append(result); + } + gotResults = true; + qDebug() << "results received, closing connection"; + clientConnection->close(); + }); + + qint64 byteswritten = clientConnection->write(block); + bool bytesflushed = clientConnection->flush(); + qDebug() << "block flushed" << byteswritten << bytesflushed; + }); + + qDebug() << "Listening on pipe" << serverName; + if (!m_linkServer.listen(serverName)) + { + qDebug() << "Unable to start local pipe server on" << serverName << ":" << m_linkServer.errorString(); + return; + } + + ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); + connect(linkFileProcess, + &ExternalLinkFileProcess::processExited, + this, + [this, gotResults]() { emit finishedPrivileged(gotResults); }); + connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); + + linkFileProcess->start(); + } + + void ExternalLinkFileProcess::runLinkFile() + { + QString fileLinkExe = PathCombine(QCoreApplication::instance()->applicationDirPath(), + BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink"); + QString params = "-s " + m_server; + + params += " -H " + QVariant(m_useHardLinks).toString(); + +#if defined Q_OS_WIN32 + SHELLEXECUTEINFO ShExecInfo; + + fileLinkExe = fileLinkExe + ".exe"; + + qDebug() << "Running: runas" << fileLinkExe << params; + + LPCWSTR programNameWin = (const wchar_t*)fileLinkExe.utf16(); + LPCWSTR paramsWin = (const wchar_t*)params.utf16(); + + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa + ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + ShExecInfo.hwnd = NULL; // Optional. A handle to the owner window, used to display and position any UI that the + // system might produce while executing this function. + ShExecInfo.lpVerb = L"runas"; // elevate to admin, show UAC + ShExecInfo.lpFile = programNameWin; + ShExecInfo.lpParameters = paramsWin; + ShExecInfo.lpDirectory = NULL; + ShExecInfo.nShow = SW_HIDE; + ShExecInfo.hInstApp = NULL; + + ShellExecuteEx(&ShExecInfo); + + WaitForSingleObject(ShExecInfo.hProcess, INFINITE); + CloseHandle(ShExecInfo.hProcess); +#endif + + qDebug() << "Process exited"; + } + + bool moveByCopy(const QString& source, const QString& dest) + { + if (!copy(source, dest)()) + { // copy + qDebug() << "Copy of" << source << "to" << dest << "failed!"; + return false; + } + if (!deletePath(source)) + { // remove original + qDebug() << "Deletion of" << source << "failed!"; + return false; + }; + return true; + } + + bool move(const QString& source, const QString& dest) + { + std::error_code err; + + ensureFilePathExists(dest); + fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); + + if (err.value() != 0) + { + if (moveByCopy(source, dest)) + return true; + qDebug() << "Move of" << source << "to" << dest << "failed!"; + qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) + << QString::number(err.value()); + return false; + } + return true; + } + + bool deletePath(QString path) + { + std::error_code err; + + fs::remove_all(StringUtils::toStdString(path), err); + + if (err) + { + qWarning() << "Failed to remove files:" << QString::fromStdString(err.message()); + } + + return err.value() == 0; + } + + bool trash(QString path, QString* pathInTrash) + { +#if defined(Q_OS_LINUX) + // Flatpak trash support via org.freedesktop.portal.Trash D-Bus interface + // See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Trash.html + if (DesktopServices::isFlatpak()) + { + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) + { + qWarning() << "D-Bus session bus not connected for Flatpak trash"; + return false; + } + + // Open the file to get a file descriptor + int fd = open(path.toUtf8().constData(), O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + qWarning() << "Failed to open file for trashing:" << path; + return false; + } + + QDBusInterface trashInterface("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Trash", + bus); + + if (!trashInterface.isValid()) + { + close(fd); + qWarning() << "Trash portal interface not available"; + return false; + } + + QDBusUnixFileDescriptor dbusfd(fd); + close(fd); // D-Bus has duplicated the fd + + QDBusReply reply = trashInterface.call("TrashFile", QVariant::fromValue(dbusfd)); + if (!reply.isValid()) + { + qWarning() << "Trash portal call failed:" << reply.error().message(); + return false; + } + + // Return value: 1 = success, 0 = failure + if (reply.value() == 1) + { + if (pathInTrash) + *pathInTrash = QString(); // Flatpak portal doesn't provide trash path + return true; + } + return false; + } +#endif +#if defined Q_OS_WIN32 + if (IsWindowsServer()) + return false; +#endif + return QFile::moveToTrash(path, pathInTrash); + } + + QString PathCombine(const QString& path1, const QString& path2) + { + if (!path1.size()) + return path2; + if (!path2.size()) + return path1; + return QDir::cleanPath(path1 + QDir::separator() + path2); + } + + QString PathCombine(const QString& path1, const QString& path2, const QString& path3) + { + return PathCombine(PathCombine(path1, path2), path3); + } + + QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4) + { + return PathCombine(PathCombine(path1, path2, path3), path4); + } + + QString AbsolutePath(const QString& path) + { + return QFileInfo(path).absolutePath(); + } + + int pathDepth(const QString& path) + { + if (path.isEmpty()) + return 0; + + QFileInfo info(path); + + auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); + + int numParts = parts.length(); + numParts -= parts.count("."); + numParts -= parts.count("..") * 2; + + return numParts; + } + + QString pathTruncate(const QString& path, int depth) + { + if (path.isEmpty() || (depth < 0)) + return ""; + + QString trunc = QFileInfo(path).path(); + + if (pathDepth(trunc) > depth) + { + return pathTruncate(trunc, depth); + } + + auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); + + if (parts.startsWith(".") && !path.startsWith(".")) + { + parts.removeFirst(); + } + if (QDir::toNativeSeparators(path).startsWith(QDir::separator())) + { + parts.prepend(""); + } + + trunc = parts.join(QDir::separator()); + + return trunc; + } + + QString ResolveExecutable(QString path) + { + if (path.isEmpty()) + { + return QString(); + } + if (!path.contains('/')) + { + path = QStandardPaths::findExecutable(path); + } + QFileInfo pathInfo(path); + if (!pathInfo.exists() || !pathInfo.isExecutable()) + { + return QString(); + } + return pathInfo.absoluteFilePath(); + } + + /** + * Normalize path + * + * Any paths inside the current folder will be normalized to relative paths (to current) + * Other paths will be made absolute + */ + QString NormalizePath(QString path) + { + QDir a = QDir::currentPath(); + QString currentAbsolute = a.absolutePath(); + + QDir b(path); + QString newAbsolute = b.absolutePath(); + + if (newAbsolute.startsWith(currentAbsolute)) + { + return a.relativeFilePath(newAbsolute); + } + else + { + return newAbsolute; + } + } + + static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; + static const QString BAD_NTFS_CHARS = "<>:\"|?*"; + static const QString BAD_HFS_CHARS = ":"; + + static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/"; + + QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) + { + for (int i = 0; i < string.length(); i++) + if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) + string[i] = replaceWith; + return string; + } + + QString RemoveInvalidPathChars(QString path, QChar replaceWith) + { + QString invalidChars; +#ifdef Q_OS_WIN + invalidChars = BAD_WIN_CHARS; +#endif + + // the null character is ignored in this check as it was not a problem until now + switch (statFS(path).fsType) + { + case FilesystemType::FAT: // similar to NTFS + /* fallthrough */ + case FilesystemType::NTFS: + /* fallthrough */ + case FilesystemType::REFS: // similar to NTFS(should be available only on windows) + invalidChars += BAD_NTFS_CHARS; + break; + // case FilesystemType::EXT: + // case FilesystemType::EXT_2_OLD: + // case FilesystemType::EXT_2_3_4: + // case FilesystemType::XFS: + // case FilesystemType::BTRFS: + // case FilesystemType::NFS: + // case FilesystemType::ZFS: + case FilesystemType::APFS: + /* fallthrough */ + case FilesystemType::HFS: + /* fallthrough */ + case FilesystemType::HFSPLUS: + /* fallthrough */ + case FilesystemType::HFSX: invalidChars += BAD_HFS_CHARS; break; + // case FilesystemType::FUSEBLK: + // case FilesystemType::F2FS: + // case FilesystemType::UNKNOWN: + default: break; + } + + if (invalidChars.size() != 0) + { + for (int i = 0; i < path.length(); i++) + { + if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) + { + path[i] = replaceWith; + } + } + } + + return path; + } + + QString DirNameFromString(QString string, QString inDir) + { + int num = 0; + QString baseName = RemoveInvalidFilenameChars(string, '-'); + QString dirName; + do + { + if (num == 0) + { + dirName = baseName; + } + else + { + dirName = baseName + "(" + QString::number(num) + ")"; + } + + // If it's over 9000 + if (num > 9000) + return ""; + num++; + } + while (QFileInfo(PathCombine(inDir, dirName)).exists()); + return dirName; + } + + // Does the folder path contain any '!'? If yes, return true, otherwise false. + // (This is a problem for Java) + bool checkProblemticPathJava(QDir folder) + { + QString pathfoldername = folder.absolutePath(); + return pathfoldername.contains("!", Qt::CaseInsensitive); + } + + QString getDesktopDir() + { + return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + } + + QString getApplicationsDir() + { + return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); + } + + QString quoteArgs(const QStringList& args, + const QString& wrap, + const QString& escapeChar, + bool wrapOnlyIfNeeded = false) + { + QString result; + + auto size = args.size(); + for (int i = 0; i < size; ++i) + { + QString arg = args[i]; + arg.replace(wrap, escapeChar); + + bool needsWrapping = !wrapOnlyIfNeeded || arg.contains(' ') || arg.contains('\t') || arg.contains(wrap); + + if (needsWrapping) + result += wrap + arg + wrap; + else + result += arg; + + if (i < size - 1) + result += ' '; + } + + return result; + } + + // Cross-platform Shortcut creation + QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) + { + if (destination.isEmpty()) + { + destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); + } + if (!ensureFilePathExists(destination)) + { + qWarning() << "Destination path can't be created!"; + return QString(); + } +#if defined(Q_OS_MACOS) + QDir application = destination + ".app/"; + + if (application.exists()) + { + qWarning() << "Application already exists!"; + return QString(); + } + + if (!application.mkpath(".")) + { + qWarning() << "Couldn't create application"; + return QString(); + } + + QDir content = application.path() + "/Contents/"; + QDir resources = content.path() + "/Resources/"; + QDir binaryDir = content.path() + "/MacOS/"; + QFile info(content.path() + "/Info.plist"); + + if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) + { + qWarning() << "Couldn't create directories within application"; + return QString(); + } + if (!info.open(QIODevice::WriteOnly | QIODevice::Text)) + { + qWarning() << "Failed to open file" << info.fileName() << "for writing!"; + return QString(); + } + + QFile(icon).rename(resources.path() + "/Icon.icns"); + + // Create the Command file + QString exec = binaryDir.path() + "/Run.command"; + + QFile f(exec); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) + { + qWarning() << "Failed to open file" << f.fileName() << "for writing!"; + return QString(); + } + QTextStream stream(&f); + + auto argstring = quoteArgs(args, "\"", "\\\""); + + stream << "#!/bin/bash" + << "\n"; + stream << "\"" << target << "\" " << argstring << "\n"; + + stream.flush(); + f.close(); + + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); + + // Generate the Info.plist + QTextStream infoStream(&info); + infoStream << " \n" + "" + "\n" + "\n" + " CFBundleExecutable\n" + " Run.command\n" // The path to the executable + " CFBundleIconFile\n" + " Icon.icns\n" + " CFBundleName\n" + " " + << name + << "\n" // Name of the application + " CFBundlePackageType\n" + " APPL\n" + " CFBundleShortVersionString\n" + " 1.0\n" + " CFBundleVersion\n" + " 1.0\n" + "\n" + ""; + + return application.path(); +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated + destination += ".desktop"; + QFile f(destination); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) + { + qWarning() << "Failed to open file '" << f.fileName() << "' for writing!"; + return QString(); + } + QTextStream stream(&f); + + auto argstring = quoteArgs(args, "'", "'\\''"); + + stream << "[Desktop Entry]" + << "\n"; + stream << "Type=Application" + << "\n"; + stream << "Categories=Game;ActionGame;AdventureGame;Simulation" + << "\n"; + stream << "Exec=\"" << target.toLocal8Bit() << "\" " << argstring.toLocal8Bit() << "\n"; + stream << "Name=" << name.toLocal8Bit() << "\n"; + if (!icon.isEmpty()) + { + stream << "Icon=" << icon.toLocal8Bit() << "\n"; + } + + stream.flush(); + f.close(); + + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); + + return destination; +#elif defined(Q_OS_WIN) + QFileInfo targetInfo(target); + + if (!targetInfo.exists()) + { + qWarning() << "Target file does not exist!"; + return QString(); + } + + target = targetInfo.absoluteFilePath(); + + if (target.length() >= MAX_PATH) + { + qWarning() << "Target file path is too long!"; + return QString(); + } + + if (!icon.isEmpty() && icon.length() >= MAX_PATH) + { + qWarning() << "Icon path is too long!"; + return QString(); + } + + destination += ".lnk"; + + if (destination.length() >= MAX_PATH) + { + qWarning() << "Destination path is too long!"; + return QString(); + } + + auto argStr = quoteArgs(args, "\"", "\\\"", true); + if (argStr.length() >= MAX_PATH) + { + qWarning() << "Arguments string is too long!"; + return QString(); + } + + HRESULT hres; + + // ...yes, you need to initialize the entire COM stack just to make a shortcut + hres = CoInitialize(nullptr); + if (FAILED(hres)) + { + qWarning() << "Failed to initialize COM!"; + return QString(); + } + + WCHAR wsz[MAX_PATH]; + + IShellLink* psl; + + // create an IShellLink instance - this stores the shortcut's attributes + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); + if (SUCCEEDED(hres)) + { + wmemset(wsz, 0, MAX_PATH); + target.toWCharArray(wsz); + psl->SetPath(wsz); + + wmemset(wsz, 0, MAX_PATH); + argStr.toWCharArray(wsz); + psl->SetArguments(wsz); + + wmemset(wsz, 0, MAX_PATH); + targetInfo.absolutePath().toWCharArray(wsz); + psl->SetWorkingDirectory(wsz); // "Starts in" attribute + + if (!icon.isEmpty()) + { + wmemset(wsz, 0, MAX_PATH); + icon.toWCharArray(wsz); + psl->SetIconLocation(wsz, 0); + } + + // query an IPersistFile interface from our IShellLink instance + // this is the interface that will actually let us save the shortcut to disk! + IPersistFile* ppf; + hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); + if (SUCCEEDED(hres)) + { + wmemset(wsz, 0, MAX_PATH); + destination.toWCharArray(wsz); + hres = ppf->Save(wsz, TRUE); + if (FAILED(hres)) + { + qWarning() << "IPresistFile->Save() failed"; + qWarning() << "hres = " << hres; + } + ppf->Release(); + } + else + { + qWarning() << "Failed to query IPersistFile interface from IShellLink instance"; + qWarning() << "hres = " << hres; + } + psl->Release(); + } + else + { + qWarning() << "Failed to create IShellLink instance"; + qWarning() << "hres = " << hres; + } + + // go away COM, nobody likes you + CoUninitialize(); + + if (SUCCEEDED(hres)) + return destination; + return QString(); +#else + qWarning("Desktop Shortcuts not supported on your platform!"); + return QString(); +#endif + } + + bool overrideFolder(QString overwritten_path, QString override_path) + { + using copy_opts = fs::copy_options; + + if (!FS::ensureFolderPathExists(overwritten_path)) + return false; + + std::error_code err; + fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing; + + // NOTE: std::copy may not overwrite existing files on GNU libstdc++ on Windows + fs::copy(StringUtils::toStdString(override_path), StringUtils::toStdString(overwritten_path), opt, err); + + if (err) + { + qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); + qCritical() << "Reason:" << QString::fromStdString(err.message()); + } + + return err.value() == 0; + } + + QString getFilesystemTypeName(FilesystemType type) + { + auto iter = s_filesystem_type_names.constFind(type); + if (iter != s_filesystem_type_names.constEnd()) + { + return iter.value().constFirst(); + } + return getFilesystemTypeName(FilesystemType::UNKNOWN); + } + + FilesystemType getFilesystemTypeFuzzy(const QString& name) + { + for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) + { + auto fs_names = iter.value(); + for (auto fs_name : fs_names) + { + if (name.toUpper().contains(fs_name.toUpper())) + return iter.key(); + } + } + return FilesystemType::UNKNOWN; + } + + FilesystemType getFilesystemType(const QString& name) + { + for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) + { + auto fs_names = iter.value(); + if (fs_names.contains(name.toUpper())) + return iter.key(); + } + return FilesystemType::UNKNOWN; + } + + /** + * @brief path to the near ancestor that exists + * + */ + QString nearestExistentAncestor(const QString& path) + { + if (QFileInfo::exists(path)) + return path; + + QDir dir(path); + if (!dir.makeAbsolute()) + return {}; + do + { + dir.setPath(QDir::cleanPath(dir.filePath(QStringLiteral("..")))); + } + while (!dir.exists() && !dir.isRoot()); + + return dir.exists() ? dir.path() : QString(); + } + + /** + * @brief colect information about the filesystem under a file + * + */ + FilesystemInfo statFS(const QString& path) + { + FilesystemInfo info; + + QStorageInfo storage_info(nearestExistentAncestor(path)); + + info.fsTypeName = storage_info.fileSystemType(); + + info.fsType = getFilesystemTypeFuzzy(info.fsTypeName); + + info.blockSize = storage_info.blockSize(); + info.bytesAvailable = storage_info.bytesAvailable(); + info.bytesFree = storage_info.bytesFree(); + info.bytesTotal = storage_info.bytesTotal(); + + info.name = storage_info.name(); + info.rootPath = storage_info.rootPath(); + + return info; + } + + /** + * @brief if the Filesystem is reflink/clone capable + * + */ + bool canCloneOnFS(const QString& path) + { + FilesystemInfo info = statFS(path); + return canCloneOnFS(info); + } + bool canCloneOnFS(const FilesystemInfo& info) + { + return canCloneOnFS(info.fsType); + } + bool canCloneOnFS(FilesystemType type) + { + return s_clone_filesystems.contains(type); + } + + /** + * @brief if the Filesystem is reflink/clone capable and both paths are on the same device + * + */ + bool canClone(const QString& src, const QString& dst) + { + auto srcVInfo = statFS(src); + auto dstVInfo = statFS(dst); + + bool sameDevice = srcVInfo.rootPath == dstVInfo.rootPath; + + return sameDevice && canCloneOnFS(srcVInfo) && canCloneOnFS(dstVInfo); + } + + /** + * @brief reflink/clones a directory and it's contents from src to dest + * @param offset subdirectory form src to copy to dest + * @return if there was an error during the filecopy + */ + bool clone::operator()(const QString& offset, bool dryRun) + { + if (!canClone(m_src.absolutePath(), m_dst.absolutePath())) + { + qWarning() << "Can not clone: not same device or not clone/reflink filesystem"; + qDebug() << "Source path:" << m_src.absolutePath(); + qDebug() << "Destination path:" << m_dst.absolutePath(); + emit cloneFailed(m_src.absolutePath(), m_dst.absolutePath()); + return false; + } + + m_cloned = 0; // reset counter + m_failedClones.clear(); + + auto src = PathCombine(m_src.absolutePath(), offset); + auto dst = PathCombine(m_dst.absolutePath(), offset); + + std::error_code err; + + // Function that'll do the actual cloneing + auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) + { + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) + return; + + auto dst_path = PathCombine(dst, relative_dst_path); + if (!dryRun) + { + ensureFilePathExists(dst_path); + clone_file(src_path, dst_path, err); + } + if (err) + { + qDebug() << "Failed to clone files: error" << err.value() << "message" + << QString::fromStdString(err.message()); + qDebug() << "Source file:" << src_path; + qDebug() << "Destination file:" << dst_path; + m_failedClones.append(qMakePair(src_path, dst_path)); + emit cloneFailed(src_path, dst_path); + return; + } + m_cloned++; + emit fileCloned(src_path, dst_path); + }; + + // We can't use copy_opts::recursive because we need to take into account the + // blacklisted paths, so we iterate over the source directory, and if there's no blacklist + // match, we copy the file. + QDir src_dir(src); + QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); + + while (source_it.hasNext()) + { + auto src_path = source_it.next(); + auto relative_path = src_dir.relativeFilePath(src_path); + + cloneFile(src_path, relative_path); + } + + // If the root src is not a directory, the previous iterator won't run. + if (!fs::is_directory(StringUtils::toStdString(src))) + cloneFile(src, ""); + + return err.value() == 0; + } + + /** + * @brief clone/reflink file from src to dst + * + */ + bool clone_file(const QString& src, const QString& dst, std::error_code& ec) + { + auto src_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(src).absoluteFilePath())); + auto dst_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(dst).absoluteFilePath())); + + FilesystemInfo srcinfo = statFS(src); + FilesystemInfo dstinfo = statFS(dst); + + if ((srcinfo.rootPath != dstinfo.rootPath) || (srcinfo.fsType != dstinfo.fsType)) + { + ec = std::make_error_code(std::errc::not_supported); + qWarning() << "reflink/clone must be to the same device and filesystem! src and dst root filesystems do " + "not match."; + return false; + } + +#if defined(Q_OS_WIN) + + if (!win_ioctl_clone(src_path, dst_path, ec)) + { + qDebug() << "failed win_ioctl_clone"; + qWarning() << "clone/reflink not supported on windows outside of btrfs or ReFS!"; + qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!"; + return false; + } + +#elif defined(Q_OS_LINUX) + + if (!linux_ficlone(src_path, dst_path, ec)) + { + qDebug() << "failed linux_ficlone:"; + return false; + } + +#elif defined(Q_OS_MACOS) + + if (!macos_bsd_clonefile(src_path, dst_path, ec)) + { + qDebug() << "failed macos_bsd_clonefile:"; + return false; + } + +#else + + qWarning() << "clone/reflink not supported! unknown OS"; + ec = std::make_error_code(std::errc::not_supported); + return false; + +#endif + + return true; + } + +#if defined(Q_OS_WIN) + + static long RoundUpToPowerOf2(long originalValue, long roundingMultiplePowerOf2) + { + long mask = roundingMultiplePowerOf2 - 1; + return (originalValue + mask) & ~mask; + } + + bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec) + { + /** + * This algorithm inspired from https://github.com/0xbadfca11/reflink + * LICENSE MIT + * + * Additional references + * https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file + * https://github.com/microsoft/CopyOnWrite/blob/main/lib/Windows/WindowsCopyOnWriteFilesystem.cs#L94 + */ + + HANDLE hSourceFile = + CreateFileW(src_path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); + if (hSourceFile == INVALID_HANDLE_VALUE) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to open source file" << src_path.c_str(); + return false; + } + + ULONG fs_flags; + if (!GetVolumeInformationByHandleW(hSourceFile, nullptr, 0, nullptr, nullptr, &fs_flags, nullptr, 0)) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to get Filesystem information for " << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + if (!(fs_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) + { + SetLastError(ERROR_NOT_CAPABLE); + ec = std::error_code(GetLastError(), std::system_category()); + qWarning() << "Filesystem at " << src_path.c_str() << " does not support reflink"; + CloseHandle(hSourceFile); + return false; + } + + FILE_END_OF_FILE_INFO sourceFileLength; + if (!GetFileSizeEx(hSourceFile, &sourceFileLength.EndOfFile)) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to size of source file" << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + FILE_BASIC_INFO sourceFileBasicInfo; + if (!GetFileInformationByHandleEx(hSourceFile, + FileBasicInfo, + &sourceFileBasicInfo, + sizeof(sourceFileBasicInfo))) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to source file info" << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + ULONG junk; + FSCTL_GET_INTEGRITY_INFORMATION_BUFFER sourceFileIntegrity; + if (!DeviceIoControl(hSourceFile, + FSCTL_GET_INTEGRITY_INFORMATION, + nullptr, + 0, + &sourceFileIntegrity, + sizeof(sourceFileIntegrity), + &junk, + nullptr)) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to source file integrity info" << src_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + + HANDLE hDestFile = CreateFileW(dst_path.c_str(), + GENERIC_READ | GENERIC_WRITE | DELETE, + 0, + nullptr, + CREATE_NEW, + 0, + hSourceFile); + + if (hDestFile == INVALID_HANDLE_VALUE) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to open dest file" << dst_path.c_str(); + CloseHandle(hSourceFile); + return false; + } + FILE_DISPOSITION_INFO destFileDispose = { TRUE }; + if (!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose))) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest file info" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + + if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &junk, nullptr)) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest sparseness" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + FSCTL_SET_INTEGRITY_INFORMATION_BUFFER setDestFileintegrity = { sourceFileIntegrity.ChecksumAlgorithm, + sourceFileIntegrity.Reserved, + sourceFileIntegrity.Flags }; + if (!DeviceIoControl(hDestFile, + FSCTL_SET_INTEGRITY_INFORMATION, + &setDestFileintegrity, + sizeof(setDestFileintegrity), + nullptr, + 0, + nullptr, + nullptr)) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest file integrity info" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + if (!SetFileInformationByHandle(hDestFile, FileEndOfFileInfo, &sourceFileLength, sizeof(sourceFileLength))) + { + ec = std::error_code(GetLastError(), std::system_category()); + qDebug() << "Failed to set dest file size" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + + const LONG64 splitThreshold = (1LL << 32) - sourceFileIntegrity.ClusterSizeInBytes; + + DUPLICATE_EXTENTS_DATA dupExtent; + dupExtent.FileHandle = hSourceFile; + for (LONG64 offset = 0, + remain = + RoundUpToPowerOf2(sourceFileLength.EndOfFile.QuadPart, sourceFileIntegrity.ClusterSizeInBytes); + remain > 0; + offset += splitThreshold, remain -= splitThreshold) + { + dupExtent.SourceFileOffset.QuadPart = dupExtent.TargetFileOffset.QuadPart = offset; + dupExtent.ByteCount.QuadPart = std::min(splitThreshold, remain); + + if (!DeviceIoControl(hDestFile, + FSCTL_DUPLICATE_EXTENTS_TO_FILE, + &dupExtent, + sizeof(dupExtent), + nullptr, + 0, + &junk, + nullptr)) + { + DWORD err = GetLastError(); + QString additionalMessage; + if (err == ERROR_BLOCK_TOO_MANY_REFERENCES) + { + static const int MaxClonesPerFile = 8175; + additionalMessage = + QString(" This is ERROR_BLOCK_TOO_MANY_REFERENCES and may mean you have surpassed the maximum " + "allowed %1 references for a single file. " + "See " + "https://docs.microsoft.com/en-us/windows-server/storage/refs/" + "block-cloning#functionality-restrictions-and-remarks") + .arg(MaxClonesPerFile); + } + ec = std::error_code(err, std::system_category()); + qDebug() << "Failed copy-on-write cloning of" << src_path.c_str() << "to" << dst_path.c_str() + << "with error" << err << additionalMessage; + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + } + + if (!(sourceFileBasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) + { + FILE_SET_SPARSE_BUFFER setDestSparse = { FALSE }; + if (!DeviceIoControl(hDestFile, + FSCTL_SET_SPARSE, + &setDestSparse, + sizeof(setDestSparse), + nullptr, + 0, + &junk, + nullptr)) + { + qDebug() << "Failed to set dest file sparseness" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + } + + sourceFileBasicInfo.CreationTime.QuadPart = 0; + if (!SetFileInformationByHandle(hDestFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) + { + qDebug() << "Failed to set dest file creation time" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + if (!FlushFileBuffers(hDestFile)) + { + qDebug() << "Failed to flush dest file buffer" << dst_path.c_str(); + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + return false; + } + destFileDispose = { FALSE }; + bool result = + !!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose)); + + CloseHandle(hSourceFile); + CloseHandle(hDestFile); + + return result; + } + +#elif defined(Q_OS_LINUX) + + bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec) + { + // https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html + + int src_fd = open(src_path.c_str(), O_RDONLY); + if (src_fd == -1) + { + qDebug() << "Failed to open file:" << src_path.c_str(); + qDebug() << "Error:" << strerror(errno); + ec = std::make_error_code(static_cast(errno)); + return false; + } + int dst_fd = open(dst_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + if (dst_fd == -1) + { + qDebug() << "Failed to open file:" << dst_path.c_str(); + qDebug() << "Error:" << strerror(errno); + ec = std::make_error_code(static_cast(errno)); + close(src_fd); + return false; + } + // attempt to clone + if (ioctl(dst_fd, FICLONE, src_fd) == -1) + { + qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str(); + qDebug() << "Error:" << strerror(errno); + ec = std::make_error_code(static_cast(errno)); + close(src_fd); + close(dst_fd); + return false; + } + if (close(src_fd)) + { + qDebug() << "Failed to close file:" << src_path.c_str(); + qDebug() << "Error:" << strerror(errno); + } + if (close(dst_fd)) + { + qDebug() << "Failed to close file:" << dst_path.c_str(); + qDebug() << "Error:" << strerror(errno); + } + return true; + } + +#elif defined(Q_OS_MACOS) + + bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec) + { + // clonefile(const char * src, const char * dst, int flags); + // https://www.manpagez.com/man/2/clonefile/ + + qDebug() << "attempting file clone via clonefile" << src_path.c_str() << "to" << dst_path.c_str(); + if (clonefile(src_path.c_str(), dst_path.c_str(), 0) == -1) + { + qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str(); + qDebug() << "Error:" << strerror(errno); + ec = std::make_error_code(static_cast(errno)); + return false; + } + return true; + } +#endif + + /** + * @brief if the Filesystem is symlink capable + * + */ + bool canLinkOnFS(const QString& path) + { + FilesystemInfo info = statFS(path); + return canLinkOnFS(info); + } + bool canLinkOnFS(const FilesystemInfo& info) + { + return canLinkOnFS(info.fsType); + } + bool canLinkOnFS(FilesystemType type) + { + return !s_non_link_filesystems.contains(type); + } + /** + * @brief if the Filesystem is symlink capable on both ends + * + */ + bool canLink(const QString& src, const QString& dst) + { + return canLinkOnFS(src) && canLinkOnFS(dst); + } + + uintmax_t hardLinkCount(const QString& path) + { + std::error_code err; + int count = fs::hard_link_count(StringUtils::toStdString(path), err); + if (err) + { + qWarning() << "Failed to count hard links for" << path << ":" << QString::fromStdString(err.message()); + count = 0; + } + return count; + } + +#ifdef Q_OS_WIN + // returns 8.3 file format from long path + QString shortPathName(const QString& file) + { + auto input = file.toStdWString(); + std::wstring output; + long length = GetShortPathNameW(input.c_str(), NULL, 0); + if (length == 0) + return {}; + // NOTE: this resizing might seem weird... + // when GetShortPathNameW fails, it returns length including null character + // when it succeeds, it returns length excluding null character + // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx + output.resize(length); + if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0) + return {}; + output.resize(length - 1); + QString ret = QString::fromStdWString(output); + return ret; + } + + // if the string survives roundtrip through local 8bit encoding... + bool fitsInLocal8bit(const QString& string) + { + return string == QString::fromLocal8Bit(string.toLocal8Bit()); + } + + QString getPathNameInLocal8bit(const QString& file) + { + if (!fitsInLocal8bit(file)) + { + auto path = shortPathName(file); + if (!path.isEmpty()) + { + return path; + } + // in case shortPathName fails just return the path as is + } + return file; + } +#endif + + QString getUniqueResourceName(const QString& filePath) + { + auto newFileName = filePath; + if (!newFileName.endsWith(".disabled")) + { + return newFileName; // prioritize enabled mods + } + newFileName.chop(9); + if (!QFile::exists(newFileName)) + { + return filePath; + } + QFileInfo fileInfo(filePath); + auto baseName = fileInfo.completeBaseName(); + auto path = fileInfo.absolutePath(); + + int counter = 1; + do + { + if (counter == 1) + { + newFileName = FS::PathCombine(path, baseName + ".duplicate"); + } + else + { + newFileName = FS::PathCombine(path, baseName + ".duplicate" + QString::number(counter)); + } + counter++; + } + while (QFile::exists(newFileName)); + + return newFileName; + } +} // namespace FS diff --git a/archived/projt-launcher/launcher/FileSystem.h b/archived/projt-launcher/launcher/FileSystem.h new file mode 100644 index 0000000000..208c459945 --- /dev/null +++ b/archived/projt-launcher/launcher/FileSystem.h @@ -0,0 +1,650 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ======================================================================== */ + +#pragma once + +#include "Exception.h" +#include "Filter.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace FS +{ + + class FileSystemException : public ::Exception + { + public: + FileSystemException(const QString& message) : Exception(message) + {} + }; + + /** + * write data to a file safely + */ + void write(const QString& filename, const QByteArray& data); + + /** + * append data to a file safely + */ + void appendSafe(const QString& filename, const QByteArray& data); + + /** + * append data to a file + */ + void append(const QString& filename, const QByteArray& data); + + /** + * read data from a file safely + */ + QByteArray read(const QString& filename); + + /** + * Update the last changed timestamp of an existing file + */ + bool updateTimestamp(const QString& filename); + + /** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a file name and is ignored! + */ + bool ensureFilePathExists(QString filenamepath); + + /** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a folder name and is created! + */ + bool ensureFolderPathExists(const QFileInfo folderPath); + + /** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a folder name and is created! + */ + bool ensureFolderPathExists(const QString folderPathName); + + /** + * @brief Copies a directory and it's contents from src to dest + */ + class copy : public QObject + { + Q_OBJECT + public: + copy(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) + { + m_src.setPath(src); + m_dst.setPath(dst); + } + copy& followSymlinks(const bool follow) + { + m_followSymlinks = follow; + return *this; + } + copy& matcher(Filter filter) + { + m_matcher = std::move(filter); + return *this; + } + copy& whitelist(bool whitelist) + { + m_whitelist = whitelist; + return *this; + } + copy& overwrite(const bool overwrite) + { + m_overwrite = overwrite; + return *this; + } + + bool operator()(bool dryRun = false) + { + return operator()(QString(), dryRun); + } + + qsizetype totalCopied() + { + return m_copied; + } + qsizetype totalFailed() + { + return m_failedPaths.length(); + } + QStringList failed() + { + return m_failedPaths; + } + + signals: + void fileCopied(const QString& relativeName); + void copyFailed(const QString& relativeName); + // NOTE: A 'shouldCopy' signal could be added here in the future for granular control. + + private: + bool operator()(const QString& offset, bool dryRun = false); + + private: + bool m_followSymlinks = true; + Filter m_matcher = nullptr; + bool m_whitelist = false; + bool m_overwrite = false; + QDir m_src; + QDir m_dst; + qsizetype m_copied; + QStringList m_failedPaths; + }; + + struct LinkPair + { + QString src; + QString dst; + }; + + struct LinkResult + { + QString src; + QString dst; + QString err_msg; + int err_value; + }; + + class ExternalLinkFileProcess : public QThread + { + Q_OBJECT + public: + ExternalLinkFileProcess(QString server, bool useHardLinks, QObject* parent = nullptr) + : QThread(parent), + m_useHardLinks(useHardLinks), + m_server(server) + {} + + void run() override + { + runLinkFile(); + emit processExited(); + } + + signals: + void processExited(); + + private: + void runLinkFile(); + + bool m_useHardLinks = false; + + QString m_server; + }; + + /** + * @brief links (a file / a directory and it's contents) from src to dest + */ + class create_link : public QObject + { + Q_OBJECT + public: + create_link(const QList path_pairs, QObject* parent = nullptr) : QObject(parent) + { + m_path_pairs.append(path_pairs); + } + create_link(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) + { + LinkPair pair = { src, dst }; + m_path_pairs.append(pair); + } + create_link& useHardLinks(const bool useHard) + { + m_useHardLinks = useHard; + return *this; + } + create_link& matcher(Filter filter) + { + m_matcher = std::move(filter); + return *this; + } + create_link& whitelist(bool whitelist) + { + m_whitelist = whitelist; + return *this; + } + create_link& linkRecursively(bool recursive) + { + m_recursive = recursive; + return *this; + } + create_link& setMaxDepth(int depth) + { + m_max_depth = depth; + return *this; + } + create_link& debug(bool d) + { + m_debug = d; + return *this; + } + + std::error_code getOSError() + { + return m_os_err; + } + + bool operator()(bool dryRun = false) + { + return operator()(QString(), dryRun); + } + + int totalLinked() + { + return m_linked; + } + int totalToLink() + { + return static_cast(m_links_to_make.size()); + } + + void runPrivileged() + { + runPrivileged(QString()); + } + void runPrivileged(const QString& offset); + + QList getResults() + { + return m_path_results; + } + + signals: + void fileLinked(const QString& srcName, const QString& dstName); + void linkFailed(const QString& srcName, const QString& dstName, const QString& err_msg, int err_value); + void finished(); + void finishedPrivileged(bool gotResults); + + private: + bool operator()(const QString& offset, bool dryRun = false); + void make_link_list(const QString& offset); + bool make_links(); + + private: + bool m_useHardLinks = false; + Filter m_matcher = nullptr; + bool m_whitelist = false; + bool m_recursive = true; + + /// @brief >= -1 = infinite, 0 = link files at src/* to dest/*, 1 = link files at src/*/* to dest/*/*, etc. + int m_max_depth = -1; + + QList m_path_pairs; + QList m_path_results; + QList m_links_to_make; + + int m_linked; + bool m_debug = false; + std::error_code m_os_err; + + QLocalServer m_linkServer; + }; + + /** + * @brief moves a file by renaming it + * @param source source file path + * @param dest destination filepath + * + */ + bool move(const QString& source, const QString& dest); + + /** + * Delete a folder recursively + */ + bool deletePath(QString path); + + /** + * Trash a folder / file + */ + bool trash(QString path, QString* pathInTrash = nullptr); + + QString PathCombine(const QString& path1, const QString& path2); + QString PathCombine(const QString& path1, const QString& path2, const QString& path3); + QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4); + + QString AbsolutePath(const QString& path); + + /** + * @brief depth of path. "foo.txt" -> 0 , "bar/foo.txt" -> 1, /baz/bar/foo.txt -> 2, etc. + * + * @param path path to measure + * @return int number of components before base path + */ + int pathDepth(const QString& path); + + /** + * @brief cut off segments of path until it is a max of length depth + * + * @param path path to truncate + * @param depth max depth of new path + * @return QString truncated path + */ + QString pathTruncate(const QString& path, int depth); + + /** + * Resolve an executable + * + * Will resolve: + * single executable (by name) + * relative path + * absolute path + * + * @return absolute path to executable or null string + */ + QString ResolveExecutable(QString path); + + /** + * Normalize path + * + * Any paths inside the current directory will be normalized to relative paths (to current) + * Other paths will be made absolute + * + * Returns false if the path logic somehow filed (and normalizedPath in invalid) + */ + QString NormalizePath(QString path); + + QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); + + QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-'); + + QString DirNameFromString(QString string, QString inDir = "."); + + /// Checks if the a given Path contains "!" + bool checkProblemticPathJava(QDir folder); + + // Get the Directory representing the User's Desktop + QString getDesktopDir(); + + // Get the Directory representing the User's Applications directory + QString getApplicationsDir(); + + // Overrides one folder with the contents of another, preserving items exclusive to the first folder + // Equivalent to doing QDir::rename, but allowing for overrides + bool overrideFolder(QString overwritten_path, QString override_path); + + /** + * Creates a shortcut to the specified target file at the specified destination path. + * Returns null QString if creation failed; otherwise returns the path to the created shortcut. + */ + QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); + + enum class FilesystemType + { + FAT, + NTFS, + REFS, + EXT, + EXT_2_OLD, + EXT_2_3_4, + XFS, + BTRFS, + NFS, + ZFS, + APFS, + HFS, + HFSPLUS, + HFSX, + FUSEBLK, + F2FS, + BCACHEFS, + UNKNOWN + }; + + /** + * @brief Ordered Mapping of enum types to reported filesystem names + * this mapping is non exsaustive, it just attempts to capture the filesystems which could be reasonalbly be in use + * . all string values are in uppercase, use `QString.toUpper()` or equivalent during lookup. + * + * QMap is ordered + * + */ + static const QMap s_filesystem_type_names = { + { FilesystemType::FAT, { "FAT" } }, + { FilesystemType::NTFS, { "NTFS" } }, + { FilesystemType::REFS, { "REFS" } }, + { FilesystemType::EXT_2_OLD, { "EXT_2_OLD", "EXT2_OLD" } }, + { FilesystemType::EXT_2_3_4, { "EXT2/3/4", "EXT_2_3_4", "EXT2", "EXT3", "EXT4" } }, + { FilesystemType::EXT, { "EXT" } }, + { FilesystemType::XFS, { "XFS" } }, + { FilesystemType::BTRFS, { "BTRFS" } }, + { FilesystemType::NFS, { "NFS" } }, + { FilesystemType::ZFS, { "ZFS" } }, + { FilesystemType::APFS, { "APFS" } }, + { FilesystemType::HFS, { "HFS" } }, + { FilesystemType::HFSPLUS, { "HFSPLUS" } }, + { FilesystemType::HFSX, { "HFSX" } }, + { FilesystemType::FUSEBLK, { "FUSEBLK" } }, + { FilesystemType::F2FS, { "F2FS" } }, + { FilesystemType::BCACHEFS, { "BCACHEFS" } }, + { FilesystemType::UNKNOWN, { "UNKNOWN" } } + }; + + /** + * @brief Get the string name of Filesystem enum object + * + * @param type + * @return QString + */ + QString getFilesystemTypeName(FilesystemType type); + + /** + * @brief Get the Filesystem enum object from a name + * Does a lookup of the type name and returns an exact match + * + * @param name + * @return FilesystemType + */ + FilesystemType getFilesystemType(const QString& name); + + /** + * @brief Get the Filesystem enum object from a name + * Does a fuzzy lookup of the type name and returns an apropreate match + * + * @param name + * @return FilesystemType + */ + FilesystemType getFilesystemTypeFuzzy(const QString& name); + + struct FilesystemInfo + { + FilesystemType fsType = FilesystemType::UNKNOWN; + QString fsTypeName; + int blockSize; + qint64 bytesAvailable; + qint64 bytesFree; + qint64 bytesTotal; + QString name; + QString rootPath; + }; + + /** + * @brief path to the near ancestor that exists + * + */ + QString nearestExistentAncestor(const QString& path); + + /** + * @brief colect information about the filesystem under a file + * + */ + FilesystemInfo statFS(const QString& path); + + static const QList s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, + FilesystemType::ZFS, FilesystemType::XFS, + FilesystemType::REFS, FilesystemType::BCACHEFS }; + + /** + * @brief if the Filesystem is reflink/clone capable + * + */ + bool canCloneOnFS(const QString& path); + bool canCloneOnFS(const FilesystemInfo& info); + bool canCloneOnFS(FilesystemType type); + + /** + * @brief if the Filesystems are reflink/clone capable and both are on the same device + * + */ + bool canClone(const QString& src, const QString& dst); + + /** + * @brief Copies a directory and it's contents from src to dest + */ + class clone : public QObject + { + Q_OBJECT + public: + clone(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) + { + m_src.setPath(src); + m_dst.setPath(dst); + } + clone& matcher(Filter filter) + { + m_matcher = std::move(filter); + return *this; + } + clone& whitelist(bool whitelist) + { + m_whitelist = whitelist; + return *this; + } + + bool operator()(bool dryRun = false) + { + return operator()(QString(), dryRun); + } + + qsizetype totalCloned() + { + return m_cloned; + } + qsizetype totalFailed() + { + return m_failedClones.length(); + } + + QList> failed() + { + return m_failedClones; + } + + signals: + void fileCloned(const QString& src, const QString& dst); + void cloneFailed(const QString& src, const QString& dst); + + private: + bool operator()(const QString& offset, bool dryRun = false); + + private: + Filter m_matcher = nullptr; + bool m_whitelist = false; + QDir m_src; + QDir m_dst; + qsizetype m_cloned; + QList> m_failedClones; + }; + + /** + * @brief clone/reflink file from src to dst + * + */ + bool clone_file(const QString& src, const QString& dst, std::error_code& ec); + +#if defined(Q_OS_WIN) + bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec); +#elif defined(Q_OS_LINUX) + bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec); +#elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec); +#endif + + static const QList s_non_link_filesystems = { + FilesystemType::FAT, + }; + + /** + * @brief if the Filesystem is symlink capable + * + */ + bool canLinkOnFS(const QString& path); + bool canLinkOnFS(const FilesystemInfo& info); + bool canLinkOnFS(FilesystemType type); + + /** + * @brief if the Filesystem is symlink capable on both ends + * + */ + bool canLink(const QString& src, const QString& dst); + + uintmax_t hardLinkCount(const QString& path); + +#ifdef Q_OS_WIN + QString getPathNameInLocal8bit(const QString& file); +#endif + + QString getUniqueResourceName(const QString& filePath); + +} // namespace FS diff --git a/archived/projt-launcher/launcher/Filter.h b/archived/projt-launcher/launcher/Filter.h new file mode 100644 index 0000000000..cdf3a361b9 --- /dev/null +++ b/archived/projt-launcher/launcher/Filter.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +using Filter = std::function; + +namespace Filters +{ + inline Filter inverse(Filter filter) + { + return [filter = std::move(filter)](const QString& src) { return !filter(src); }; + } + + inline Filter any(QList filters) + { + return [filters = std::move(filters)](const QString& src) + { + for (auto& filter : filters) + if (filter(src)) + return true; + + return false; + }; + } + + inline Filter equals(QString pattern) + { + return [pattern = std::move(pattern)](const QString& src) { return src == pattern; }; + } + + inline Filter equalsAny(QStringList patterns = {}) + { + return [patterns = std::move(patterns)](const QString& src) + { return patterns.isEmpty() || patterns.contains(src); }; + } + + inline Filter equalsOrEmpty(QString pattern) + { + return [pattern = std::move(pattern)](const QString& src) { return src.isEmpty() || src == pattern; }; + } + + inline Filter contains(QString pattern) + { + return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; + } + + inline Filter startsWith(QString pattern) + { + return [pattern = std::move(pattern)](const QString& src) { return src.startsWith(pattern); }; + } + + inline Filter regexp(QRegularExpression pattern) + { + return [pattern = std::move(pattern)](const QString& src) { return pattern.match(src).hasMatch(); }; + } +} // namespace Filters diff --git a/archived/projt-launcher/launcher/GZip.cpp b/archived/projt-launcher/launcher/GZip.cpp new file mode 100644 index 0000000000..0df1e18638 --- /dev/null +++ b/archived/projt-launcher/launcher/GZip.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ +#include "GZip.h" +#include +#include +#include +#include + +bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) +{ + if (compressedBytes.size() == 0) + { + uncompressedBytes = compressedBytes; + return true; + } + + unsigned uncompLength = compressedBytes.size(); + uncompressedBytes.clear(); + uncompressedBytes.resize(uncompLength); + + z_stream strm; + memset(&strm, 0, sizeof(strm)); + strm.next_in = (Bytef*)compressedBytes.data(); + strm.avail_in = compressedBytes.size(); + + bool done = false; + + if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK) + { + return false; + } + + int err = Z_OK; + + while (!done) + { + // If our output buffer is too small + if (strm.total_out >= uncompLength) + { + uncompressedBytes.resize(uncompLength * 2); + uncompLength *= 2; + } + + strm.next_out = reinterpret_cast((uncompressedBytes.data() + strm.total_out)); + strm.avail_out = uncompLength - strm.total_out; + + // Inflate another chunk. + err = inflate(&strm, Z_SYNC_FLUSH); + if (err == Z_STREAM_END) + done = true; + else if (err != Z_OK) + { + break; + } + } + + if (inflateEnd(&strm) != Z_OK || !done) + { + return false; + } + + uncompressedBytes.resize(strm.total_out); + return true; +} + +bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) +{ + if (uncompressedBytes.size() == 0) + { + compressedBytes = uncompressedBytes; + return true; + } + + unsigned compLength = qMin(uncompressedBytes.size(), 16); + compressedBytes.clear(); + compressedBytes.resize(compLength); + + z_stream zs; + memset(&zs, 0, sizeof(zs)); + + if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (16 + MAX_WBITS), 8, Z_DEFAULT_STRATEGY) != Z_OK) + { + return false; + } + + zs.next_in = (Bytef*)uncompressedBytes.data(); + zs.avail_in = uncompressedBytes.size(); + + int ret; + compressedBytes.resize(uncompressedBytes.size()); + + unsigned offset = 0; + unsigned temp = 0; + do + { + auto remaining = compressedBytes.size() - offset; + if (remaining < 1) + { + compressedBytes.resize(compressedBytes.size() * 2); + } + zs.next_out = reinterpret_cast((compressedBytes.data() + offset)); + temp = zs.avail_out = compressedBytes.size() - offset; + ret = deflate(&zs, Z_FINISH); + offset += temp - zs.avail_out; + } + while (ret == Z_OK); + + compressedBytes.resize(offset); + + if (deflateEnd(&zs) != Z_OK) + { + return false; + } + + if (ret != Z_STREAM_END) + { + return false; + } + return true; +} + +int inf(QFile* source, std::function handleBlock) +{ + constexpr auto CHUNK = 16384; + int ret; + unsigned have; + z_stream strm; + memset(&strm, 0, sizeof(strm)); + char in[CHUNK]; + unsigned char out[CHUNK]; + + ret = inflateInit2(&strm, (16 + MAX_WBITS)); + if (ret != Z_OK) + return ret; + + /* decompress until deflate stream ends or end of file */ + do + { + strm.avail_in = source->read(in, CHUNK); + if (source->error()) + { + (void)inflateEnd(&strm); + return Z_ERRNO; + } + if (strm.avail_in == 0) + break; + strm.next_in = reinterpret_cast(in); + + /* run inflate() on input until output buffer not full */ + do + { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = inflate(&strm, Z_NO_FLUSH); + assert(ret != Z_STREAM_ERROR); /* state not clobbered */ + switch (ret) + { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; + [[fallthrough]]; + case Z_DATA_ERROR: + case Z_MEM_ERROR: (void)inflateEnd(&strm); return ret; + } + have = CHUNK - strm.avail_out; + if (!handleBlock(QByteArray(reinterpret_cast(out), have))) + { + (void)inflateEnd(&strm); + return Z_OK; + } + } + while (strm.avail_out == 0); + + /* done when inflate() says it's done */ + } + while (ret != Z_STREAM_END); + + /* clean up and return */ + (void)inflateEnd(&strm); + return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; +} + +QString zerr(int ret) +{ + switch (ret) + { + case Z_ERRNO: return QObject::tr("error handling file"); + case Z_STREAM_ERROR: return QObject::tr("invalid compression level"); + case Z_DATA_ERROR: return QObject::tr("invalid or incomplete deflate data"); + case Z_MEM_ERROR: return QObject::tr("out of memory"); + case Z_VERSION_ERROR: return QObject::tr("zlib version mismatch!"); + } + return {}; +} + +QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock) +{ + auto ret = inf(source, handleBlock); + return zerr(ret); +} diff --git a/archived/projt-launcher/launcher/GZip.h b/archived/projt-launcher/launcher/GZip.h new file mode 100644 index 0000000000..831ddab5fb --- /dev/null +++ b/archived/projt-launcher/launcher/GZip.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include + +namespace GZip +{ + + bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); + bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); + QString readGzFileByBlocks(QFile* source, std::function handleBlock); + +} // namespace GZip diff --git a/archived/projt-launcher/launcher/HardwareInfo.cpp b/archived/projt-launcher/launcher/HardwareInfo.cpp new file mode 100644 index 0000000000..90fd9c4aad --- /dev/null +++ b/archived/projt-launcher/launcher/HardwareInfo.cpp @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Octol1ttle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "HardwareInfo.h" + +#include +#include + +#include +#include +#include +#include +#include + +#if !defined(Q_OS_MACOS) && __has_include() && __has_include() && __has_include() +#define PROJT_HAS_VULKAN_INFO 1 +#include +#include +#include +#endif + +namespace { +bool vulkanInfo(QStringList& out) +{ +#ifdef PROJT_HAS_VULKAN_INFO + QVulkanInstance inst; + if (!inst.create()) { + qWarning() << "Vulkan instance creation failed, VkResult:" << inst.errorCode(); + out << "Couldn't get Vulkan device information"; + return false; + } + + QVulkanWindow window; + window.setVulkanInstance(&inst); + + for (auto device : window.availablePhysicalDevices()) { + const auto supportedVulkanVersion = QVersionNumber(VK_API_VERSION_MAJOR(device.apiVersion), VK_API_VERSION_MINOR(device.apiVersion), + VK_API_VERSION_PATCH(device.apiVersion)); + out << QString("Found Vulkan device: %1 (API version %2)").arg(device.deviceName).arg(supportedVulkanVersion.toString()); + } +#else + out << "Vulkan device information unavailable at build time"; + return false; +#endif + + return true; +} + +bool openGlInfo(QStringList& out) +{ + QOpenGLContext ctx; + if (!ctx.create()) { + qWarning() << "OpenGL context creation failed"; + out << "Couldn't get OpenGL device information"; + return false; + } + + QOffscreenSurface surface; + surface.create(); + ctx.makeCurrent(&surface); + + auto* f = ctx.functions(); + f->initializeOpenGLFunctions(); + + auto toQString = [](const GLubyte* str) { return QString(reinterpret_cast(str)); }; + out << "OpenGL driver vendor: " + toQString(f->glGetString(GL_VENDOR)); + out << "OpenGL renderer: " + toQString(f->glGetString(GL_RENDERER)); + out << "OpenGL driver version: " + toQString(f->glGetString(GL_VERSION)); + + return true; +} +} // namespace + +#ifndef Q_OS_LINUX +QStringList HardwareInfo::gpuInfo() +{ + QStringList info; + vulkanInfo(info); + openGlInfo(info); + return info; +} +#endif + +#ifdef Q_OS_WINDOWS +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include "windows.h" + +QString HardwareInfo::cpuInfo() +{ + const QSettings registry(R"(HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0)", QSettings::NativeFormat); + return registry.value("ProcessorNameString").toString(); +} + +uint64_t HardwareInfo::totalRamMiB() +{ + MEMORYSTATUSEX status; + status.dwLength = sizeof status; + + if (GlobalMemoryStatusEx(&status) == TRUE) { + // transforming bytes -> mib + return status.ullTotalPhys / 1024 / 1024; + } + + qWarning() << "Could not get total RAM: GlobalMemoryStatusEx"; + return 0; +} + +uint64_t HardwareInfo::availableRamMiB() +{ + MEMORYSTATUSEX status; + status.dwLength = sizeof status; + + if (GlobalMemoryStatusEx(&status) == TRUE) { + // transforming bytes -> mib + return status.ullAvailPhys / 1024 / 1024; + } + + qWarning() << "Could not get available RAM: GlobalMemoryStatusEx"; + return 0; +} + +#elif defined(Q_OS_MACOS) +#include "mach/mach.h" +#include "sys/sysctl.h" + +QString HardwareInfo::cpuInfo() +{ + std::array buffer; + size_t bufferSize = buffer.size(); + if (sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferSize, nullptr, 0) == 0) { + return QString(buffer.data()); + } + + qWarning() << "Could not get CPU model: sysctlbyname"; + return ""; +} + +uint64_t HardwareInfo::totalRamMiB() +{ + uint64_t memsize; + size_t memsizeSize = sizeof memsize; + if (sysctlbyname("hw.memsize", &memsize, &memsizeSize, nullptr, 0) == 0) { + // transforming bytes -> mib + return memsize / 1024 / 1024; + } + + qWarning() << "Could not get total RAM: sysctlbyname"; + return 0; +} + +uint64_t HardwareInfo::availableRamMiB() +{ + mach_port_t host_port = mach_host_self(); + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + + vm_statistics64_data_t vm_stats; + + if (host_statistics64(host_port, HOST_VM_INFO64, reinterpret_cast(&vm_stats), &count) == KERN_SUCCESS) { + // transforming bytes -> mib + return (vm_stats.free_count + vm_stats.inactive_count) * vm_page_size / 1024 / 1024; + } + + qWarning() << "Could not get available RAM: host_statistics64"; + return 0; +} + +#elif defined(Q_OS_LINUX) +#include + +namespace { +QString afterColon(QString& str) +{ + return str.remove(0, str.indexOf(':') + 2).trimmed(); +} +} // namespace + +QString HardwareInfo::cpuInfo() +{ + std::ifstream cpuin("/proc/cpuinfo"); + for (std::string line; std::getline(cpuin, line);) { + // model name : AMD Ryzen 7 5800X 8-Core Processor + if (QString str = QString::fromStdString(line); str.startsWith("model name")) { + return afterColon(str); + } + } + + qWarning() << "Could not get CPU model: /proc/cpuinfo"; + return "unknown"; +} + +uint64_t readMemInfo(QString searchTarget) +{ + std::ifstream memin("/proc/meminfo"); + for (std::string line; std::getline(memin, line);) { + // MemTotal: 16287480 kB + if (QString str = QString::fromStdString(line); str.startsWith(searchTarget)) { + bool ok = false; + const uint total = str.simplified().section(' ', 1, 1).toUInt(&ok); + if (!ok) { + qWarning() << "Could not read /proc/meminfo: failed to parse string:" << str; + return 0; + } + + // transforming kib -> mib + return total / 1024; + } + } + + qWarning() << "Could not read /proc/meminfo: search target not found:" << searchTarget; + return 0; +} + +uint64_t HardwareInfo::totalRamMiB() +{ + return readMemInfo("MemTotal"); +} + +uint64_t HardwareInfo::availableRamMiB() +{ + return readMemInfo("MemAvailable"); +} + +QStringList HardwareInfo::gpuInfo() +{ + QStringList list; + const bool vulkanSuccess = vulkanInfo(list); + const bool openGlSuccess = openGlInfo(list); + if (vulkanSuccess || openGlSuccess) { + return list; + } + + std::array buffer; + FILE* lspci = popen("lspci -k", "r"); + + if (!lspci) { + return { "Could not detect GPUs: lspci is not present" }; + } + + bool readingGpuInfo = false; + QString currentModel = ""; + while (fgets(buffer.data(), 512, lspci) != nullptr) { + QString str(buffer.data()); + // clang-format off + // 04:00.0 VGA compatible controller: Advanced Micro Devices, Inc. [AMD/ATI] Ellesmere [Radeon RX 470/480/570/570X/580/580X/590] (rev e7) + // Subsystem: Sapphire Technology Limited Radeon RX 580 Pulse 4GB + // Kernel driver in use: amdgpu + // Kernel modules: amdgpu + // clang-format on + if (str.contains("VGA compatible controller")) { + readingGpuInfo = true; + } else if (!str.startsWith('\t')) { + readingGpuInfo = false; + } + if (!readingGpuInfo) { + continue; + } + + if (str.contains("Subsystem")) { + currentModel = "Found GPU: " + afterColon(str); + } + if (str.contains("Kernel driver in use")) { + currentModel += " (using driver " + afterColon(str); + } + if (str.contains("Kernel modules")) { + currentModel += ", available drivers: " + afterColon(str) + ")"; + list.append(currentModel); + } + } + pclose(lspci); + return list; +} + +#else + +QString HardwareInfo::cpuInfo() +{ + return "unknown"; +} + +#if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#include + +uint64_t HardwareInfo::totalRamMiB() +{ + char buff[512]; + FILE* fp = popen("sysctl hw.physmem", "r"); + if (fp != nullptr) { + if (fgets(buff, 512, fp) != nullptr) { + std::string str(buff); + uint64_t mem = std::stoull(str.substr(12, std::string::npos)); + + // transforming kib -> mib + return mem / 1024; + } + } + + return 0; +} + +#else +uint64_t HardwareInfo::totalRamMiB() +{ + return 0; +} +#endif + +uint64_t HardwareInfo::availableRamMiB() +{ + return 0; +} + +#endif diff --git a/archived/projt-launcher/launcher/HardwareInfo.h b/archived/projt-launcher/launcher/HardwareInfo.h new file mode 100644 index 0000000000..9069080c7b --- /dev/null +++ b/archived/projt-launcher/launcher/HardwareInfo.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Octol1ttle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +namespace HardwareInfo { +QString cpuInfo(); +uint64_t totalRamMiB(); +uint64_t availableRamMiB(); +QStringList gpuInfo(); +} // namespace HardwareInfo diff --git a/archived/projt-launcher/launcher/InstanceCopyPrefs.cpp b/archived/projt-launcher/launcher/InstanceCopyPrefs.cpp new file mode 100644 index 0000000000..48f3c465d3 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceCopyPrefs.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "InstanceCopyPrefs.h" + +bool InstanceCopyPrefs::allTrue() const +{ + return copySaves && keepPlaytime && copyGameOptions && copyResourcePacks && copyShaderPacks && copyServers + && copyMods && copyScreenshots; +} + +// Returns a single RegEx string of the selected folders/files to filter out (ex: +// ".minecraft/saves|.minecraft/server.dat") +QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const +{ + return getSelectedFiltersAsRegex({}); +} +QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const +{ + QStringList filters; + + if (!copySaves) + filters << "saves"; + + if (!copyGameOptions) + filters << "options.txt"; + + if (!copyResourcePacks) + filters << "resourcepacks" + << "texturepacks"; + + if (!copyShaderPacks) + filters << "shaderpacks"; + + if (!copyServers) + filters << "servers.dat" + << "servers.dat_old" + << "server-resource-packs"; + + if (!copyMods) + filters << "coremods" + << "mods" + << "config"; + + if (!copyScreenshots) + filters << "screenshots"; + + for (auto filter : additionalFilters) + { + filters << filter; + } + + // If we have any filters to add, join them as a single regex string to return: + if (!filters.isEmpty()) + { + const QString MC_ROOT = "[.]?minecraft/"; + // Ensure first filter starts with root, then join other filters with OR regex before root (ex: + // ".minecraft/saves|.minecraft/mods"): + return MC_ROOT + filters.join("|" + MC_ROOT); + } + + return {}; +} + +// ======= Getters ======= +bool InstanceCopyPrefs::isCopySavesEnabled() const +{ + return copySaves; +} + +bool InstanceCopyPrefs::isKeepPlaytimeEnabled() const +{ + return keepPlaytime; +} + +bool InstanceCopyPrefs::isCopyGameOptionsEnabled() const +{ + return copyGameOptions; +} + +bool InstanceCopyPrefs::isCopyResourcePacksEnabled() const +{ + return copyResourcePacks; +} + +bool InstanceCopyPrefs::isCopyShaderPacksEnabled() const +{ + return copyShaderPacks; +} + +bool InstanceCopyPrefs::isCopyServersEnabled() const +{ + return copyServers; +} + +bool InstanceCopyPrefs::isCopyModsEnabled() const +{ + return copyMods; +} + +bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const +{ + return copyScreenshots; +} + +bool InstanceCopyPrefs::isUseSymLinksEnabled() const +{ + return useSymLinks; +} + +bool InstanceCopyPrefs::isUseHardLinksEnabled() const +{ + return useHardLinks; +} + +bool InstanceCopyPrefs::isLinkRecursivelyEnabled() const +{ + return linkRecursively; +} + +bool InstanceCopyPrefs::isDontLinkSavesEnabled() const +{ + return dontLinkSaves; +} + +bool InstanceCopyPrefs::isUseCloneEnabled() const +{ + return useClone; +} + +// ======= Setters ======= +void InstanceCopyPrefs::enableCopySaves(bool b) +{ + copySaves = b; +} + +void InstanceCopyPrefs::enableKeepPlaytime(bool b) +{ + keepPlaytime = b; +} + +void InstanceCopyPrefs::enableCopyGameOptions(bool b) +{ + copyGameOptions = b; +} + +void InstanceCopyPrefs::enableCopyResourcePacks(bool b) +{ + copyResourcePacks = b; +} + +void InstanceCopyPrefs::enableCopyShaderPacks(bool b) +{ + copyShaderPacks = b; +} + +void InstanceCopyPrefs::enableCopyServers(bool b) +{ + copyServers = b; +} + +void InstanceCopyPrefs::enableCopyMods(bool b) +{ + copyMods = b; +} + +void InstanceCopyPrefs::enableCopyScreenshots(bool b) +{ + copyScreenshots = b; +} + +void InstanceCopyPrefs::enableUseSymLinks(bool b) +{ + useSymLinks = b; +} + +void InstanceCopyPrefs::enableLinkRecursively(bool b) +{ + linkRecursively = b; +} + +void InstanceCopyPrefs::enableUseHardLinks(bool b) +{ + useHardLinks = b; +} + +void InstanceCopyPrefs::enableDontLinkSaves(bool b) +{ + dontLinkSaves = b; +} + +void InstanceCopyPrefs::enableUseClone(bool b) +{ + useClone = b; +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/InstanceCopyPrefs.h b/archived/projt-launcher/launcher/InstanceCopyPrefs.h new file mode 100644 index 0000000000..c97997c73d --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceCopyPrefs.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +struct InstanceCopyPrefs +{ + public: + bool allTrue() const; + QString getSelectedFiltersAsRegex() const; + QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; + // Getters + bool isCopySavesEnabled() const; + bool isKeepPlaytimeEnabled() const; + bool isCopyGameOptionsEnabled() const; + bool isCopyResourcePacksEnabled() const; + bool isCopyShaderPacksEnabled() const; + bool isCopyServersEnabled() const; + bool isCopyModsEnabled() const; + bool isCopyScreenshotsEnabled() const; + bool isUseSymLinksEnabled() const; + bool isLinkRecursivelyEnabled() const; + bool isUseHardLinksEnabled() const; + bool isDontLinkSavesEnabled() const; + bool isUseCloneEnabled() const; + // Setters + void enableCopySaves(bool b); + void enableKeepPlaytime(bool b); + void enableCopyGameOptions(bool b); + void enableCopyResourcePacks(bool b); + void enableCopyShaderPacks(bool b); + void enableCopyServers(bool b); + void enableCopyMods(bool b); + void enableCopyScreenshots(bool b); + void enableUseSymLinks(bool b); + void enableLinkRecursively(bool b); + void enableUseHardLinks(bool b); + void enableDontLinkSaves(bool b); + void enableUseClone(bool b); + + protected: // data + bool copySaves = true; + bool keepPlaytime = true; + bool copyGameOptions = true; + bool copyResourcePacks = true; + bool copyShaderPacks = true; + bool copyServers = true; + bool copyMods = true; + bool copyScreenshots = true; + bool useSymLinks = false; + bool linkRecursively = false; + bool useHardLinks = false; + bool dontLinkSaves = false; + bool useClone = false; +}; diff --git a/archived/projt-launcher/launcher/InstanceCopyTask.cpp b/archived/projt-launcher/launcher/InstanceCopyTask.cpp new file mode 100644 index 0000000000..2b202bf4a6 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceCopyTask.cpp @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "InstanceCopyTask.h" +#include +#include +#include +#include "FileSystem.h" +#include "Filter.h" +#include "NullInstance.h" +#include "minecraft/MinecraftInstance.h" +#include "settings/INISettingsObject.h" +#include "tasks/Task.h" + +InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) +{ + m_origInstance = origInstance; + m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); + m_useLinks = prefs.isUseSymLinksEnabled(); + m_linkRecursively = prefs.isLinkRecursivelyEnabled(); + m_useHardLinks = prefs.isLinkRecursivelyEnabled() && prefs.isUseHardLinksEnabled(); + m_copySaves = prefs.isLinkRecursivelyEnabled() && prefs.isDontLinkSavesEnabled() && prefs.isCopySavesEnabled(); + m_useClone = prefs.isUseCloneEnabled(); + + QString filters = prefs.getSelectedFiltersAsRegex(); + if (m_useLinks || m_useHardLinks) + { + if (!filters.isEmpty()) + filters += "|"; + filters += "instance.cfg"; + } + + qDebug() << "CopyFilters:" << filters; + + if (!filters.isEmpty()) + { + // Set regex filter: + // NOTE: Regex for filtering files during copy. + QRegularExpression regexp(filters, QRegularExpression::CaseInsensitiveOption); + m_matcher = Filters::regexp(regexp); + } + + // Store the original instance type to create the correct instance type during copy + m_instanceType = m_origInstance->instanceType(); +} + +void InstanceCopyTask::executeTask() +{ + setStatus(tr("Copying instance %1").arg(m_origInstance->name())); + + m_copyFuture = QtConcurrent::run( + QThreadPool::globalInstance(), + [this] + { + if (m_useClone) + { + FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); + folderClone.matcher(m_matcher); + + folderClone(true); + setProgress(0, folderClone.totalCloned()); + connect(&folderClone, + &FS::clone::fileCloned, + [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); + return folderClone(); + } + if (m_useLinks || m_useHardLinks) + { + std::unique_ptr savesCopy; + if (m_copySaves) + { + QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); + + QString staging_mc_dir; + if (dotMCDir.exists() && !mcDir.exists()) + staging_mc_dir = dotMCDir.filePath(); + else + staging_mc_dir = mcDir.filePath(); + + savesCopy = std::make_unique(FS::PathCombine(m_origInstance->gameRoot(), "saves"), + FS::PathCombine(staging_mc_dir, "saves")); + savesCopy->followSymlinks(true); + (*savesCopy)(true); + setProgress(0, savesCopy->totalCopied()); + connect(savesCopy.get(), + &FS::copy::fileCopied, + [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); + } + FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); + int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the + // instance folder + folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher); + + folderLink(true); + setProgress(0, m_progressTotal + folderLink.totalToLink()); + connect(&folderLink, + &FS::create_link::fileLinked, + [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); + bool there_were_errors = false; + + if (!folderLink()) + { +#if defined Q_OS_WIN32 + if (!m_useHardLinks) + { + setProgress(0, m_progressTotal); + qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; + + qDebug() << "attempting to run with privelage"; + + QEventLoop loop; + bool got_priv_results = false; + + connect(&folderLink, + &FS::create_link::finishedPrivileged, + this, + [&got_priv_results, &loop](bool gotResults) + { + if (!gotResults) + { + qDebug() << "Privileged run exited without results!"; + } + got_priv_results = gotResults; + loop.quit(); + }); + folderLink.runPrivileged(); + + loop.exec(); // wait for the finished signal + + for (auto result : folderLink.getResults()) + { + if (result.err_value != 0) + { + there_were_errors = true; + } + } + + if (savesCopy) + { + there_were_errors |= !(*savesCopy)(); + } + + return got_priv_results && !there_were_errors; + } +#else + qDebug() << "Link Failed!" << folderLink.getOSError().value() + << folderLink.getOSError().message().c_str(); +#endif + return false; + } + + if (savesCopy) + { + there_were_errors |= !(*savesCopy)(); + } + + return !there_were_errors; + } + FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); + folderCopy.followSymlinks(false).matcher(m_matcher); + + folderCopy(true); + setProgress(0, folderCopy.totalCopied()); + connect(&folderCopy, + &FS::copy::fileCopied, + [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); + return folderCopy(); + }); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); +} + +void InstanceCopyTask::copyFinished() +{ + if (!isRunning()) + { + return; + } + auto successful = m_copyFuture.result(); + if (!successful) + { + emitFailed(tr("Instance folder copy failed.")); + return; + } + + // Load instance settings with error handling + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + if (!QFile::exists(configPath)) + { + emitFailed(tr("Instance configuration file not found: %1").arg(configPath)); + return; + } + + auto instanceSettings = std::make_shared(configPath); + if (!instanceSettings->reload()) + { + emitFailed(tr("Failed to load instance configuration")); + return; + } + + instanceSettings->registerSetting("InstanceType", ""); + QString inst_type = instanceSettings->get("InstanceType").toString(); + + InstancePtr inst; + if (inst_type == "OneSix" || inst_type.isEmpty()) + { + inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, m_stagingPath)); + } + else + { + inst.reset(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); + } + inst->setName(name()); + inst->setIconKey(m_instIcon); + if (!m_keepPlaytime) + { + inst->resetTimePlayed(); + } + if (m_useLinks) + { + inst->addLinkedInstanceId(m_origInstance->id()); + auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt")); + + QByteArray allowed_symlinks; + if (allowed_symlinks_file.exists()) + { + allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath())); + if (allowed_symlinks.right(1) != "\n") + allowed_symlinks.append("\n"); // we want to be on a new line + } + allowed_symlinks.append(m_origInstance->gameRoot().toUtf8()); + allowed_symlinks.append("\n"); + if (allowed_symlinks_file.isSymLink()) + FS::deletePath(allowed_symlinks_file.filePath()); // we dont want to modify the original. also make sure the + // resulting file is not itself a link. + + try + { + FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); + } + catch (const FS::FileSystemException& e) + { + qCritical() << "Failed to write symlink :" << e.cause(); + } + } + + emitSucceeded(); +} + +void InstanceCopyTask::copyAborted() +{ + if (!isRunning()) + { + return; + } + emitAborted(); +} + +bool InstanceCopyTask::abort() +{ + if (m_copyFutureWatcher.isRunning()) + { + m_copyFutureWatcher.cancel(); + emitAborted(); + return true; + } + return false; +} diff --git a/archived/projt-launcher/launcher/InstanceCopyTask.h b/archived/projt-launcher/launcher/InstanceCopyTask.h new file mode 100644 index 0000000000..81bb74a88f --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceCopyTask.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include "BaseInstance.h" +#include "BaseVersion.h" +#include "Filter.h" +#include "InstanceCopyPrefs.h" +#include "InstanceTask.h" +#include "net/NetJob.h" +#include "settings/SettingsObject.h" +#include "tasks/Task.h" + +class InstanceCopyTask : public InstanceTask +{ + Q_OBJECT + public: + explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs); + + protected: + //! Entry point for tasks. + virtual void executeTask() override; + bool abort() override; + void copyFinished(); + void copyAborted(); + + private: + /* data */ + InstancePtr m_origInstance; + QFuture m_copyFuture; + QFutureWatcher m_copyFutureWatcher; + Filter m_matcher; + QString m_instanceType; + bool m_keepPlaytime; + bool m_useLinks = false; + bool m_useHardLinks = false; + bool m_copySaves = false; + bool m_linkRecursively = false; + bool m_useClone = false; +}; diff --git a/archived/projt-launcher/launcher/InstanceCreationTask.cpp b/archived/projt-launcher/launcher/InstanceCreationTask.cpp new file mode 100644 index 0000000000..8bb0e20740 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceCreationTask.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "InstanceCreationTask.h" + +#include +#include + +#include "FileSystem.h" +#include "minecraft/MinecraftLoadAndCheck.h" +#include "tasks/SequentialTask.h" + +bool InstanceCreationTask::abort() +{ + if (!canAbort()) + return false; + + m_abort = true; + if (m_gameFilesTask) + return m_gameFilesTask->abort(); + + return true; +} + +void InstanceCreationTask::executeTask() +{ + setAbortable(true); + + if (updateInstance()) + { + emitSucceeded(); + return; + } + + // When the user aborted in the update stage. + if (m_abort) + { + emitAborted(); + return; + } + + m_createdInstance = createInstance(); + if (!m_createdInstance) + { + if (m_abort) + return; + + qWarning() << "Instance creation failed!"; + if (!m_error_message.isEmpty()) + { + qWarning() << "Reason: " << m_error_message; + emitFailed(tr("Error while creating new instance:\n%1").arg(m_error_message)); + } + else + { + emitFailed(tr("Error while creating new instance.")); + } + + return; + } + + // If this is set, it means we're updating an instance. So, we now need to remove the + // files scheduled to, and we'd better not let the user abort in the middle of it, since it'd + // put the instance in an invalid state. + if (shouldOverride()) + { + bool deleteFailed = false; + + setAbortable(false); + setStatus(tr("Removing old conflicting files...")); + qDebug() << "Removing old files"; + + for (const QString& path : m_files_to_remove) + { + if (!QFile::exists(path)) + continue; + + qDebug() << "Removing" << path; + + if (!QFile::remove(path)) + { + qCritical() << "Could not remove" << path; + deleteFailed = true; + } + } + + if (deleteFailed) + { + emitFailed(tr("Failed to remove old conflicting files.")); + return; + } + } + + if (m_abort) + return; + + m_createdInstance->saveNow(); + + setAbortable(true); + setAbortButtonText(tr("Skip")); + qDebug() << "Downloading game files"; + + auto updateTasks = m_createdInstance->createUpdateTask(); + if (updateTasks.isEmpty()) + { + emitSucceeded(); + return; + } + + auto task = makeShared(); + task->addTask(makeShared(m_createdInstance.get(), Net::Mode::Online)); + for (const auto& updateTask : updateTasks) + { + task->addTask(updateTask); + } + connect(task.get(), + &Task::finished, + this, + [this, task] + { + if (task->wasSuccessful() || m_abort) + { + emitSucceeded(); + } + else + { + emitFailed(tr("Could not download game files: %1").arg(task->failReason())); + } + }); + connect(task.get(), &Task::progress, this, &InstanceCreationTask::setProgress); + connect(task.get(), &Task::stepProgress, this, &InstanceCreationTask::propagateStepProgress); + connect(task.get(), &Task::status, this, &InstanceCreationTask::setStatus); + connect(task.get(), &Task::details, this, &InstanceCreationTask::setDetails); + + setDetails(tr("Downloading game files")); + m_gameFilesTask = task; + m_gameFilesTask->start(); +} diff --git a/archived/projt-launcher/launcher/InstanceCreationTask.h b/archived/projt-launcher/launcher/InstanceCreationTask.h new file mode 100644 index 0000000000..5b29ac775c --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceCreationTask.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "BaseVersion.h" +#include "InstanceTask.h" +#include "minecraft/MinecraftInstance.h" + +#include + +class InstanceCreationTask : public InstanceTask +{ + Q_OBJECT + public: + InstanceCreationTask() = default; + virtual ~InstanceCreationTask() = default; + + bool abort() override; + + protected: + void executeTask() final override; + + /** + * Tries to update an already existing instance. + * + * This can be implemented by subclasses to provide a way of updating an already existing + * instance, according to that implementation's concept of 'identity' (i.e. instances that + * are updates / downgrades of one another). + * + * If this returns true, createInstance() will not run, so you should do all update steps in here. + * Otherwise, createInstance() is run as normal. + */ + virtual bool updateInstance() + { + return false; + }; + + /** + * Creates a new instance. + * + * Returns the instance if it was created or nullptr otherwise. + */ + virtual std::unique_ptr createInstance() + { + return nullptr; + }; + + QString getError() const + { + return m_error_message; + } + + protected: + void setError(const QString& message) + { + m_error_message = message; + }; + + protected: + bool m_abort = false; + + QStringList m_files_to_remove; + + private: + QString m_error_message; + std::unique_ptr m_createdInstance; + Task::Ptr m_gameFilesTask; +}; diff --git a/archived/projt-launcher/launcher/InstanceDirUpdate.cpp b/archived/projt-launcher/launcher/InstanceDirUpdate.cpp new file mode 100644 index 0000000000..dc854d53c5 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceDirUpdate.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include "InstanceDirUpdate.h" + +#include + +#include "Application.h" +#include "FileSystem.h" + +#include "InstanceList.h" +#include "ui/dialogs/CustomMessageBox.h" + +QString askToUpdateInstanceDirName(InstancePtr instance, + const QString& oldName, + const QString& newName, + QWidget* parent) +{ + if (oldName == newName) + return QString(); + + QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString(); + if (renamingMode == "MetadataOnly") + return QString(); + + auto oldRoot = instance->instanceRoot(); + auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath()); + auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName); + if (oldRoot == newRoot) + return QString(); + if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName)) + return QString(); + + // Check for conflict + if (QDir(newRoot).exists()) + { + QMessageBox::warning( + parent, + QObject::tr("Cannot rename instance"), + QObject::tr("New instance root (%1) already exists.
Only the metadata will be renamed.") + .arg(newRoot)); + return QString(); + } + + // Ask if we should rename + if (renamingMode == "AskEverytime") + { + auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent); + auto dialog = CustomMessageBox::selectable(parent, + QObject::tr("Rename instance folder"), + QObject::tr("Would you also like to rename the instance folder?\n\n" + "Old name: %1\n" + "New name: %2") + .arg(oldName, newName), + QMessageBox::Question, + QMessageBox::No | QMessageBox::Yes, + QMessageBox::NoButton, + checkBox); + + auto res = dialog->exec(); + if (checkBox->isChecked()) + { + if (res == QMessageBox::Yes) + APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir"); + else + APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly"); + } + if (res == QMessageBox::No) + return QString(); + } + + // Check for linked instances + if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming"))) + return QString(); + + // Now we can confirm that a renaming is happening + if (!instance->syncInstanceDirName(newRoot)) + { + QMessageBox::warning(parent, + QObject::tr("Cannot rename instance"), + QObject::tr("An error occurred when performing the following renaming operation:
" + " - Old instance root: %1
" + " - New instance root: %2
" + "Only the metadata is renamed.") + .arg(oldRoot, newRoot)); + return QString(); + } + return newRoot; +} + +bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb) +{ + auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); + if (!linkedInstances.empty()) + { + auto response = CustomMessageBox::selectable( + parent, + QObject::tr("There are linked instances"), + QObject::tr("The following instance(s) might reference files in this instance:\n\n" + "%1\n\n" + "%2 it could break the other instance(s), \n\n" + "Do you wish to proceed?", + nullptr, + linkedInstances.count()) + .arg(linkedInstances.join("\n")) + .arg(verb), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + if (response != QMessageBox::Yes) + return false; + } + return true; +} diff --git a/archived/projt-launcher/launcher/InstanceDirUpdate.h b/archived/projt-launcher/launcher/InstanceDirUpdate.h new file mode 100644 index 0000000000..4f60d82f31 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceDirUpdate.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include "BaseInstance.h" + +/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened +QString askToUpdateInstanceDirName(InstancePtr instance, + const QString& oldName, + const QString& newName, + QWidget* parent); + +/// Check if there are linked instances, and display a warning; return true if the operation should proceed +bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb); diff --git a/archived/projt-launcher/launcher/InstanceImportTask.cpp b/archived/projt-launcher/launcher/InstanceImportTask.cpp new file mode 100644 index 0000000000..3a49d924d9 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceImportTask.cpp @@ -0,0 +1,648 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include "InstanceImportTask.h" + +#include +#include +#include + +#include "Application.h" +#include "FileSystem.h" +#include "Json.h" +#include "MMCZip.h" +#include "NullInstance.h" + +#include "QObjectPtr.h" +#include "icons/IconList.hpp" +#include "icons/IconUtils.hpp" + +#include "modplatform/flame/FlameInstanceCreationTask.h" +#include "modplatform/modrinth/ModrinthInstanceCreationTask.h" +#include "modplatform/technic/TechnicPackProcessor.h" + +#include "settings/INISettingsObject.h" +#include "tasks/Task.h" + +#include "net/ApiDownload.h" + +#include +#include +#include + +#include + +InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap&& extra_info) + : m_sourceUrl(sourceUrl), + m_extra_info(extra_info) +{} + +bool InstanceImportTask::abort() +{ + if (!canAbort()) + return false; + + bool wasAborted = false; + if (m_task) + wasAborted = m_task->abort(); + return wasAborted; +} + +void InstanceImportTask::executeTask() +{ + setAbortable(true); + + if (m_sourceUrl.isLocalFile()) + { + m_archivePath = m_sourceUrl.toLocalFile(); + processZipPack(); + } + else + { + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + + downloadFromUrl(); + } +} + +void InstanceImportTask::downloadFromUrl() +{ + const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); + + auto entry = APPLICATION->metacache()->resolveEntry("general", path); + entry->setStale(true); + m_archivePath = entry->getFullPath(); + + auto filesNetJob = makeShared(tr("Modpack download"), APPLICATION->network()); + filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); + + connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack); + connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress); + connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed); + connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted); + m_task.reset(filesNetJob); + filesNetJob->start(); +} + +QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root) +{ + if (!isRunning()) + { + return {}; + } + QuaZipDir rootDir(zip, root); + for (auto&& fileName : rootDir.entryList(QDir::Files)) + { + setDetails(fileName); + if (fileName == "instance.cfg") + { + qDebug() << "MultiMC:" << true; + m_modpackType = ModpackType::MultiMC; + return root; + } + if (fileName == "manifest.json") + { + qDebug() << "Flame:" << true; + m_modpackType = ModpackType::Flame; + return root; + } + + QCoreApplication::processEvents(); + } + + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) + { + if ("overrides/" == fileName) + continue; + + QString result = getRootFromZip(zip, root + fileName); + if (!result.isEmpty()) + return result; + } + + return {}; +} + +void InstanceImportTask::processZipPack() +{ + setStatus(tr("Attempting to determine instance type")); + QDir extractDir(m_stagingPath); + qDebug() << "Attempting to create instance from" << m_archivePath; + + // open the zip and find relevant files in it + auto packZip = std::make_shared(m_archivePath); + if (!packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + + QuaZipDir packZipDir(packZip.get()); + qDebug() << "Attempting to determine instance type"; + + QString root; + + // NOTE: Prioritize modpack platforms that aren't searched for recursively. + // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example + // https://docs.modrinth.com/docs/modpacks/format_definition/#storage + if (packZipDir.exists("/modrinth.index.json")) + { + // process as Modrinth pack + qDebug() << "Modrinth:" << true; + m_modpackType = ModpackType::Modrinth; + } + else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) + { + // process as Technic pack + qDebug() << "Technic:" << true; + extractDir.mkpath("minecraft"); + extractDir.cd("minecraft"); + m_modpackType = ModpackType::Technic; + } + else + { + root = getRootFromZip(packZip.get()); + setDetails(""); + } + if (m_modpackType == ModpackType::Unknown) + { + emitFailed(tr("Archive does not contain a recognized modpack type.")); + return; + } + setStatus(tr("Extracting modpack")); + + // make sure we extract just the pack + auto zipTask = makeShared(packZip, extractDir, root); + + auto progressStep = std::make_shared(); + connect(zipTask.get(), + &Task::finished, + this, + [this, progressStep] + { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished, Qt::QueuedConnection); + connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(zipTask.get(), + &Task::failed, + this, + [this, progressStep](QString reason) + { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + + connect(zipTask.get(), + &Task::progress, + this, + [this, progressStep](qint64 current, qint64 total) + { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(zipTask.get(), + &Task::status, + this, + [this, progressStep](QString status) + { + progressStep->status = status; + stepProgress(*progressStep); + }); + connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); + m_task.reset(zipTask); + zipTask->start(); +} + +void InstanceImportTask::extractFinished() +{ + setAbortable(false); + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if (file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser + | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if (origPermissions != permissions) + { + if (!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + switch (m_modpackType) + { + case ModpackType::MultiMC: processMultiMC(); return; + case ModpackType::Technic: processTechnic(); return; + case ModpackType::Flame: processFlame(); return; + case ModpackType::Modrinth: processModrinth(); return; + case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; + } +} + +bool installIcon(QString root, QString instIconKey) +{ + auto importIconPath = projt::icons::findBestIconIn(root, instIconKey); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = projt::icons::findBestIconIn(root, "icon.png"); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = projt::icons::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png"); + if (!importIconPath.isNull() && QFile::exists(importIconPath)) + { + // import icon + auto iconList = APPLICATION->icons(); + if (iconList->iconFileExists(instIconKey)) + { + iconList->deleteIcon(instIconKey); + } + iconList->installIcon(importIconPath, instIconKey + ".png"); + return true; + } + return false; +} + +void InstanceImportTask::processFlame() +{ + shared_qobject_ptr inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) + { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); + auto pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + inst_creation_task = makeShared(m_stagingPath, + m_globalSettings, + nullptr, + pack_id, + pack_version_id, + original_instance_id); + } + else + { + // Extract pack IDs from manifest.json for direct ZIP imports + QString pack_id; + QString pack_version_id; + + const auto manifestPath = FS::PathCombine(m_stagingPath, "manifest.json"); + if (QFile::exists(manifestPath)) + { + try + { + auto doc = Json::requireDocument(manifestPath); + auto obj = doc.object(); + + if (obj.contains("projectID")) + { + pack_id = QString::number(obj.value("projectID").toInt()); + } + if (obj.contains("fileID")) + { + pack_version_id = QString::number(obj.value("fileID").toInt()); + } + } + catch (const Exception& e) + { + qWarning() << "Failed to parse manifest.json for ID extraction:" << e.cause(); + } + catch (...) + { + qWarning() << "Failed to parse manifest.json for ID extraction (unknown error)"; + } + } + + inst_creation_task = + makeShared(m_stagingPath, m_globalSettings, nullptr, pack_id, pack_version_id); + } + + inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") + { + auto iconKey = QString("Flame_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) + { + m_instIcon = iconKey; + } + } + inst_creation_task->setIcon(m_instIcon); + inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); + + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), + &Task::succeeded, + this, + [this, weak] + { + if (auto sp = weak.lock()) + { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } + emitSucceeded(); + }); + connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); + + connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); + + connect(inst_creation_task.get(), + &Task::warningLogged, + this, + [this](const QString& line) { m_Warnings.append(line); }); + + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); +} + +void InstanceImportTask::processTechnic() +{ + shared_qobject_ptr packProcessor{ new Technic::TechnicPackProcessor }; + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); + packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath); +} + +void InstanceImportTask::processMultiMC() +{ + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + + NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + + // reset time played on import... because packs. + instance.resetTimePlayed(); + + // set a new nice name + instance.setName(name()); + + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + else + { + m_instIcon = instance.iconKey(); + + installIcon(instance.instanceRoot(), m_instIcon); + } + emitSucceeded(); +} + +void InstanceImportTask::processModrinth() +{ + shared_qobject_ptr inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) + { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + QString pack_version_id; + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + if (pack_version_id_it != m_extra_info.constEnd()) + pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + inst_creation_task = makeShared(m_stagingPath, + m_globalSettings, + nullptr, + pack_id, + pack_version_id, + original_instance_id); + } + else + { + QString pack_id; + QString pack_version_id; + + // 1) Best source: modrinth.index.json inside staging + const auto indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (QFile::exists(indexPath)) + { + try + { + auto doc = Json::requireDocument(indexPath); + auto obj = doc.object(); + + if (obj.contains("projectId")) + { + pack_id = obj.value("projectId").toString(); + } + if (obj.contains("versionId")) + { + pack_version_id = obj.value("versionId").toString(); + } + } + catch (const Exception& e) + { + qWarning() << "Failed to read modrinth.index.json for pack IDs:" << e.cause(); + } + catch (...) + { + qWarning() << "Failed to read modrinth.index.json for pack IDs (unknown error)"; + } + } + + // 2) Fallback: derive pack_id from source URL or local filename + if (pack_id.isEmpty() && !m_sourceUrl.isEmpty()) + { + if (m_sourceUrl.isLocalFile()) + { + pack_id = QFileInfo(m_sourceUrl.toLocalFile()).baseName(); + } + else + { + static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); + auto match = s_regex.match(m_sourceUrl.toString()); + if (match.hasMatch()) + { + pack_id = match.captured(1); + } + } + } + + // Extract version ID from modrinth.index.json for direct ZIP imports + // pack_version_id already declared above + // Attempt to read modrinth.index.json for metadata if available + try + { + QString indexFile = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (QFileInfo::exists(indexFile)) + { + auto doc = Json::requireDocument(indexFile); + auto obj = doc.object(); + // Valid Modrinth packs might not have projectID directly, but checking for it or 'name' matches + if (obj.contains("projectID")) + { + pack_id = QString::number(obj.value("projectID").toInt()); + } + else if (obj.contains("name") && pack_id.isEmpty()) + { + // Check if name contains ID-like pattern or use it as fallback? + // For now just keep existing heuristic if not found. + } + // Version extraction + if (obj.contains("versionId")) + { + pack_version_id = obj.value("versionId").toString(); + } + } + } + catch (...) + { + // Ignore format errors + } + + inst_creation_task = + makeShared(m_stagingPath, m_globalSettings, nullptr, pack_id, pack_version_id); + } + + inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") + { + auto iconKey = QString("Modrinth_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) + { + m_instIcon = iconKey; + } + } + inst_creation_task->setIcon(m_instIcon); + inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); + + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), + &Task::succeeded, + this, + [this, weak] + { + if (auto sp = weak.lock()) + { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } + emitSucceeded(); + }); + connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); + connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); + connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); + connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); + + connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); + + connect(inst_creation_task.get(), + &Task::warningLogged, + this, + [this](const QString& line) { m_Warnings.append(line); }); + + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); +} diff --git a/archived/projt-launcher/launcher/InstanceImportTask.h b/archived/projt-launcher/launcher/InstanceImportTask.h new file mode 100644 index 0000000000..20db7a3e5e --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceImportTask.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include +#include +#include +#include "InstanceTask.h" + +class QuaZip; + +class InstanceImportTask : public InstanceTask +{ + Q_OBJECT + public: + explicit InstanceImportTask(const QUrl& sourceUrl, + QWidget* parent = nullptr, + QMap&& extra_info = {}); + virtual ~InstanceImportTask() = default; + bool abort() override; + + protected: + //! Entry point for tasks. + virtual void executeTask() override; + + private: + void processMultiMC(); + void processTechnic(); + void processFlame(); + void processModrinth(); + QString getRootFromZip(QuaZip* zip, const QString& root = ""); + + private slots: + void processZipPack(); + void extractFinished(); + + private: /* data */ + QUrl m_sourceUrl; + QString m_archivePath; + Task::Ptr m_task; + enum class ModpackType + { + Unknown, + MultiMC, + Technic, + Flame, + Modrinth, + } m_modpackType = ModpackType::Unknown; + + // Extra info we might need, that's available before, but can't be derived from + // the source URL / the resource it points to alone. + QMap m_extra_info; + + void downloadFromUrl(); +}; diff --git a/archived/projt-launcher/launcher/InstanceList.cpp b/archived/projt-launcher/launcher/InstanceList.cpp new file mode 100644 index 0000000000..597fbb7a22 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceList.cpp @@ -0,0 +1,1293 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include "InstanceList.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "BaseInstance.h" +#include "ExponentialSeries.h" +#include "FileSystem.h" + +#include "InstanceTask.h" +#include "NullInstance.h" +#include "WatchLock.h" +#include "minecraft/MinecraftInstance.h" +#include "settings/INISettingsObject.h" + +#ifdef Q_OS_WIN32 +#include +#endif + +const static int GROUP_FILE_FORMAT_VERSION = 1; + +InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) + : QAbstractListModel(parent), + m_globalSettings(settings) +{ + resumeWatch(); + m_reloadDebounceTimer.setSingleShot(true); + m_reloadDebounceTimer.setInterval(300); + connect(&m_reloadDebounceTimer, &QTimer::timeout, this, &InstanceList::performDebouncedReload); + + // Create aand normalize path + if (!QDir::current().exists(instDir)) + { + QDir::current().mkpath(instDir); + } + + connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated); + + // NOTE: canonicalPath requires the path to exist. Do not move this above the creation block! + m_instDir = QDir(instDir).canonicalPath(); + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged); + m_watcher->addPath(m_instDir); +} + +InstanceList::~InstanceList() +{} + +Qt::DropActions InstanceList::supportedDragActions() const +{ + return Qt::MoveAction; +} + +Qt::DropActions InstanceList::supportedDropActions() const +{ + return Qt::MoveAction; +} + +bool InstanceList::canDropMimeData(const QMimeData* data, + [[maybe_unused]] Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) const +{ + if (data && data->hasFormat("application/x-instanceid")) + { + return true; + } + return false; +} + +bool InstanceList::dropMimeData(const QMimeData* data, + [[maybe_unused]] Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (data && data->hasFormat("application/x-instanceid")) + { + return true; + } + return false; +} + +QStringList InstanceList::mimeTypes() const +{ + auto types = QAbstractListModel::mimeTypes(); + types.push_back("application/x-instanceid"); + return types; +} + +QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const +{ + auto mimeData = QAbstractListModel::mimeData(indexes); + if (indexes.size() == 1) + { + auto instanceId = data(indexes[0], InstanceIDRole).toString(); + mimeData->setData("application/x-instanceid", instanceId.toUtf8()); + } + return mimeData; +} + +QStringList InstanceList::getLinkedInstancesById(const QString& id) const +{ + QStringList linkedInstances; + for (auto inst : m_instances) + { + if (inst->isLinkedToInstanceId(id)) + linkedInstances.append(inst->id()); + } + return linkedInstances; +} + +int InstanceList::rowCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent); + return m_instances.count(); +} + +QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const +{ + Q_UNUSED(parent); + if (row < 0 || row >= m_instances.size()) + return QModelIndex(); + return createIndex(row, column, (void*)m_instances.at(row).get()); +} + +QVariant InstanceList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + { + return QVariant(); + } + BaseInstance* pdata = static_cast(index.internalPointer()); + switch (role) + { + case InstancePointerRole: + { + QVariant v = QVariant::fromValue((void*)pdata); + return v; + } + case InstanceIDRole: + { + return pdata->id(); + } + case Qt::EditRole: + case Qt::DisplayRole: + { + return pdata->name(); + } + case Qt::AccessibleTextRole: + { + return tr("%1 Instance").arg(pdata->name()); + } + case Qt::ToolTipRole: + { + return pdata->instanceRoot(); + } + case Qt::DecorationRole: + { + return pdata->iconKey(); + } + // HACK: see InstanceView.h in gui! + case GroupRole: + { + return getInstanceGroup(pdata->id()); + } + default: break; + } + return QVariant(); +} + +bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid()) + { + return false; + } + if (role != Qt::EditRole) + { + return false; + } + BaseInstance* pdata = static_cast(index.internalPointer()); + auto newName = value.toString(); + if (pdata->name() == newName) + { + return true; + } + pdata->setName(newName); + return true; +} + +Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f; + if (index.isValid()) + { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + return f; +} + +GroupId InstanceList::getInstanceGroup(const InstanceId& id) const +{ + auto inst = getInstanceById(id); + if (!inst) + { + return GroupId(); + } + auto iter = m_instanceGroupIndex.find(inst->id()); + if (iter != m_instanceGroupIndex.end()) + { + return *iter; + } + return GroupId(); +} + +void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name) +{ + if (name.isEmpty() && !name.isNull()) + name = QString(); + + auto inst = getInstanceById(id); + if (!inst) + { + qDebug() << "Attempt to set a null instance's group"; + return; + } + + bool changed = false; + auto iter = m_instanceGroupIndex.find(inst->id()); + if (iter != m_instanceGroupIndex.end()) + { + if (*iter != name) + { + decreaseGroupCount(*iter); + *iter = name; + changed = true; + } + } + else + { + changed = true; + m_instanceGroupIndex[id] = name; + } + + if (changed) + { + increaseGroupCount(name); + auto idx = getInstIndex(inst.get()); + emit dataChanged(index(idx), index(idx), { GroupRole }); + saveGroupList(); + } +} + +QStringList InstanceList::getGroups() +{ + QStringList keys = m_groupNameCache.keys(); + keys.sort(); + return keys; +} + +void InstanceList::deleteGroup(const GroupId& name) +{ + m_groupNameCache.remove(name); + m_collapsedGroups.remove(name); + + bool removed = false; + qDebug() << "Delete group" << name; + for (auto& instance : m_instances) + { + const QString& instID = instance->id(); + const QString instGroupName = getInstanceGroup(instID); + if (instGroupName == name) + { + m_instanceGroupIndex.remove(instID); + qDebug() << "Remove" << instID << "from group" << name; + removed = true; + auto idx = getInstIndex(instance.get()); + if (idx >= 0) + emit dataChanged(index(idx), index(idx), { GroupRole }); + } + } + if (removed) + saveGroupList(); +} + +void InstanceList::renameGroup(const QString& src, const QString& dst) +{ + m_groupNameCache.remove(src); + if (m_collapsedGroups.remove(src)) + m_collapsedGroups.insert(dst); + + bool modified = false; + qDebug() << "Rename group" << src << "to" << dst; + for (auto& instance : m_instances) + { + const QString& instID = instance->id(); + const QString instGroupName = getInstanceGroup(instID); + if (instGroupName == src) + { + m_instanceGroupIndex[instID] = dst; + increaseGroupCount(dst); + qDebug() << "Set" << instID << "group to" << dst; + modified = true; + auto idx = getInstIndex(instance.get()); + if (idx >= 0) + emit dataChanged(index(idx), index(idx), { GroupRole }); + } + } + if (modified) + saveGroupList(); +} + +bool InstanceList::isGroupCollapsed(const QString& group) +{ + return m_collapsedGroups.contains(group); +} + +bool InstanceList::trashInstance(const InstanceId& id) +{ + auto inst = getInstanceById(id); + if (!inst) + { + qWarning() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; + return false; + } + + QString cachedGroupId = m_instanceGroupIndex[id]; + + qDebug() << "Will trash instance" << id; + QString trashedLoc; + + if (m_instanceGroupIndex.remove(id)) + { + decreaseGroupCount(cachedGroupId); + saveGroupList(); + } + + if (!FS::trash(inst->instanceRoot(), &trashedLoc)) + { + qWarning() << "Trash of instance" << id << "has not been completely successful..."; + return false; + } + + qDebug() << "Instance" << id << "has been trashed by the launcher."; + m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId }); + + // Also trash all of its shortcuts; we remove the shortcuts if trash fails since it is invalid anyway + for (const auto& [name, filePath, target] : inst->shortcuts()) + { + if (!FS::trash(filePath, &trashedLoc)) + { + qWarning() << "Trash of shortcut" << name << "at path" << filePath << "for instance" << id + << "has not been successful, trying to delete it instead..."; + if (!FS::deletePath(filePath)) + { + qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id + << "has not been successful, given up..."; + } + else + { + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id + << "has been deleted by the launcher."; + } + continue; + } + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id + << "has been trashed by the launcher."; + m_trashHistory.top().shortcuts.append({ { name, filePath, target }, trashedLoc }); + } + + return true; +} + +bool InstanceList::trashedSomething() const +{ + return !m_trashHistory.empty(); +} + +bool InstanceList::undoTrashInstance() +{ + if (m_trashHistory.empty()) + { + qWarning() << "Nothing to recover from trash."; + return true; + } + + auto top = m_trashHistory.pop(); + + while (QDir(top.path).exists()) + { + top.id += "1"; + top.path += "1"; + } + + if (!QFile(top.trashPath).rename(top.path)) + { + qWarning() << "Moving" << top.trashPath << "back to" << top.path << "failed!"; + return false; + } + qDebug() << "Moving" << top.trashPath << "back to" << top.path; + + bool ok = true; + for (const auto& [data, trashPath] : top.shortcuts) + { + if (QDir(data.filePath).exists()) + { + // Don't try to append 1 here as the shortcut may have suffixes like .app, just warn and skip it + qWarning() << "Shortcut" << trashPath << "original directory" << data.filePath << "already exists!"; + ok = false; + continue; + } + if (!QFile(trashPath).rename(data.filePath)) + { + qWarning() << "Moving shortcut from" << trashPath << "back to" << data.filePath << "failed!"; + ok = false; + continue; + } + qDebug() << "Moving shortcut from" << trashPath << "back to" << data.filePath; + } + + m_instanceGroupIndex[top.id] = top.groupName; + increaseGroupCount(top.groupName); + + saveGroupList(); + emit instancesChanged(); + return ok; +} + +void InstanceList::deleteInstance(const InstanceId& id) +{ + auto inst = getInstanceById(id); + if (!inst) + { + qWarning() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; + return; + } + + QString cachedGroupId = m_instanceGroupIndex[id]; + + if (m_instanceGroupIndex.remove(id)) + { + decreaseGroupCount(cachedGroupId); + saveGroupList(); + } + + qDebug() << "Will delete instance" << id; + if (!FS::deletePath(inst->instanceRoot())) + { + qWarning() << "Deletion of instance" << id << "has not been completely successful..."; + return; + } + + qDebug() << "Instance" << id << "has been deleted by the launcher."; + + for (const auto& [name, filePath, target] : inst->shortcuts()) + { + if (!FS::deletePath(filePath)) + { + qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id + << "has not been successful..."; + continue; + } + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id + << "has been deleted by the launcher."; + } +} + +static QMap getIdMapping(const QList& list) +{ + QMap out; + int i = 0; + for (auto& item : list) + { + auto id = item->id(); + if (out.contains(id)) + { + qWarning() << "Duplicate ID" << id << "in instance list"; + } + out[id] = std::make_pair(item, i); + i++; + } + return out; +} + +QList InstanceList::discoverInstances() +{ + qInfo() << "Discovering instances in" << m_instDir; + QList out; + QDirIterator iter(m_instDir, + QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, + QDirIterator::FollowSymlinks); + while (iter.hasNext()) + { + QString subDir = iter.next(); + QFileInfo dirInfo(subDir); + if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) + continue; + // if it is a symlink, ignore it if it goes to the instance folder + if (dirInfo.isSymLink()) + { + QFileInfo targetInfo(dirInfo.symLinkTarget()); + QFileInfo instDirInfo(m_instDir); + if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) + { + qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; + continue; + } + } + auto id = dirInfo.fileName(); + out.append(id); + } + instanceSet = QSet(out.begin(), out.end()); + m_instancesProbed = true; + return out; +} + +InstanceList::InstListError InstanceList::loadList() +{ + auto existingIds = getIdMapping(m_instances); + + QList newList; + + for (auto& id : discoverInstances()) + { + if (existingIds.contains(id)) + { + existingIds.remove(id); + } + else + { + InstancePtr instPtr = loadInstance(id); + if (instPtr) + { + newList.append(instPtr); + } + } + } + + // Remove instances that no longer exist on disk + if (!existingIds.isEmpty()) + { + removeDeadInstances(existingIds); + } + if (newList.size()) + { + add(newList); + } + m_dirty = false; + updateTotalPlayTime(); + return NoError; +} + +void InstanceList::removeDeadInstances(const QMap& deadInstances) +{ + if (deadInstances.isEmpty()) + { + return; + } + + // Sort by original index (descending) to remove from back to front + auto deadList = deadInstances.values(); + auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool + { return a.second > b.second; }; + std::sort(deadList.begin(), deadList.end(), orderSortPredicate); + + // Remove contiguous ranges efficiently with batch operations + int front_bookmark = -1; + int back_bookmark = -1; + int currentItem = -1; + + auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() + { + beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); + m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); + endRemoveRows(); + front_bookmark = -1; + back_bookmark = currentItem; + }; + + for (auto& removedItem : deadList) + { + auto instPtr = removedItem.first; + m_instanceMap.remove(instPtr->id()); + instPtr->invalidate(); + currentItem = removedItem.second; + + if (back_bookmark == -1) + { + back_bookmark = currentItem; + } + else if (currentItem == front_bookmark - 1) + { + // Part of contiguous sequence, continue + } + else + { + // Seam between previous and current item + removeNow(); + } + front_bookmark = currentItem; + } + + if (back_bookmark != -1) + { + removeNow(); + } +} + +QList InstanceList::getAllInstancesByManagedName(const QString& managed_name) const +{ + QList result; + for (auto instance : m_instances) + { + if (instance->getManagedPackID() == managed_name) + { + result.append(instance); + } + } + return result; +} + +void InstanceList::updateTotalPlayTime() +{ + totalPlayTime = 0; + for (auto const& itr : m_instances) + { + totalPlayTime += itr.get()->totalTimePlayed(); + } +} + +void InstanceList::saveNow() +{ + for (auto& item : m_instances) + { + item->saveNow(); + } +} + +void InstanceList::add(const QList& t) +{ + beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); + m_instances.append(t); + for (auto& ptr : t) + { + m_instanceMap.insert(ptr->id(), ptr); + connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); + } + endInsertRows(); +} + +void InstanceList::resumeWatch() +{ + if (m_watchLevel > 0) + { + qWarning() << "Bad suspend level resume in instance list"; + return; + } + m_watchLevel++; + if (m_watchLevel > 0 && m_dirty) + { + loadList(); + } +} + +void InstanceList::suspendWatch() +{ + m_watchLevel--; +} + +void InstanceList::providerUpdated() +{ + m_dirty = true; + if (m_watchLevel == 1) + { + m_reloadDebounceTimer.stop(); + loadList(); + } +} + +InstancePtr InstanceList::getInstanceById(QString instId) const +{ + if (instId.isEmpty()) + return InstancePtr(); + return m_instanceMap.value(instId); +} + +InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const +{ + if (managed_name.isEmpty()) + return {}; + + for (auto instance : m_instances) + { + if (instance->getManagedPackName() == managed_name) + return instance; + } + + return {}; +} + +QModelIndex InstanceList::getInstanceIndexById(const QString& id) const +{ + return index(getInstIndex(getInstanceById(id).get())); +} + +int InstanceList::getInstIndex(BaseInstance* inst) const +{ + int count = m_instances.count(); + for (int i = 0; i < count; i++) + { + if (inst == m_instances[i].get()) + { + return i; + } + } + return -1; +} + +void InstanceList::propertiesChanged(BaseInstance* inst) +{ + int i = getInstIndex(inst); + if (i != -1) + { + emit dataChanged(index(i), index(i)); + updateTotalPlayTime(); + } +} + +InstancePtr InstanceList::loadInstance(const InstanceId& id) +{ + if (!m_groupsLoaded) + { + loadGroupList(); + } + + auto instanceRoot = FS::PathCombine(m_instDir, id); + auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); + InstancePtr inst; + + instanceSettings->registerSetting("InstanceType", ""); + + QString inst_type = instanceSettings->get("InstanceType").toString(); + + // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that + // this is probably a OneSix instance + if (inst_type == "OneSix" || inst_type.isEmpty()) + { + inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + } + else + { + inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); + } + qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); + + auto shortcut = inst->shortcuts(); + if (!shortcut.isEmpty()) + qDebug() << "Loaded" << shortcut.size() << "shortcut(s) for instance" << inst->name(); + + return inst; +} + +void InstanceList::increaseGroupCount(const QString& group) +{ + if (group.isEmpty()) + return; + + ++m_groupNameCache[group]; +} + +void InstanceList::decreaseGroupCount(const QString& group) +{ + if (group.isEmpty()) + return; + + if (--m_groupNameCache[group] < 1) + { + m_groupNameCache.remove(group); + m_collapsedGroups.remove(group); + } +} + +void InstanceList::saveGroupList() +{ + qDebug() << "Will save group list now."; + if (!m_instancesProbed) + { + qDebug() << "Group saving prevented because we don't know the full list of instances yet."; + return; + } + WatchLock foo(m_watcher, m_instDir); + QString groupFileName = m_instDir + "/instgroups.json"; + QMap> reverseGroupMap; + for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) + { + const QString& id = iter.key(); + QString group = iter.value(); + if (group.isEmpty()) + continue; + if (!instanceSet.contains(id)) + { + qDebug() << "Skipping saving missing instance" << id << "to groups list."; + continue; + } + + if (!reverseGroupMap.count(group)) + { + QSet set; + set.insert(id); + reverseGroupMap[group] = set; + } + else + { + QSet& set = reverseGroupMap[group]; + set.insert(id); + } + } + QJsonObject toplevel; + toplevel.insert("formatVersion", QJsonValue(QString("1"))); + QJsonObject groupsArr; + for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) + { + auto list = iter.value(); + auto name = iter.key(); + QJsonObject groupObj; + QJsonArray instanceArr; + groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); + for (auto item : list) + { + instanceArr.append(QJsonValue(item)); + } + groupObj.insert("instances", instanceArr); + groupsArr.insert(name, groupObj); + } + toplevel.insert("groups", groupsArr); + // empty string represents ungrouped "group" + if (m_collapsedGroups.contains("")) + { + QJsonObject ungrouped; + ungrouped.insert("hidden", QJsonValue(true)); + toplevel.insert("ungrouped", ungrouped); + } + QJsonDocument doc(toplevel); + try + { + FS::write(groupFileName, doc.toJson()); + qDebug() << "Group list saved."; + } + catch (const FS::FileSystemException& e) + { + qCritical() << "Failed to write instance group file :" << e.cause(); + } +} + +void InstanceList::loadGroupList() +{ + qDebug() << "Will load group list now."; + + QString groupFileName = m_instDir + "/instgroups.json"; + + // if there's no group file, fail + if (!QFileInfo(groupFileName).exists()) + return; + + QByteArray jsonData; + try + { + jsonData = FS::read(groupFileName); + } + catch (const FS::FileSystemException& e) + { + qCritical() << "Failed to read instance group file :" << e.cause(); + return; + } + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); + + // if the json was bad, fail + if (error.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse instance group file: %1 at offset %2") + .arg(error.errorString(), QString::number(error.offset)) + .toUtf8(); + return; + } + + // if the root of the json wasn't an object, fail + if (!jsonDoc.isObject()) + { + qWarning() << "Invalid group file. Root entry should be an object."; + return; + } + + QJsonObject rootObj = jsonDoc.object(); + + // Make sure the format version matches, otherwise fail. + if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION) + return; + + // Get the groups. if it's not an object, fail + if (!rootObj.value("groups").isObject()) + { + qWarning() << "Invalid group list JSON: 'groups' should be an object."; + return; + } + + m_instanceGroupIndex.clear(); + m_groupNameCache.clear(); + + // Iterate through all the groups. + QJsonObject groupMapping = rootObj.value("groups").toObject(); + for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) + { + QString groupName = iter.key(); + + if (iter.key().isEmpty()) + { + qWarning() << "Redundant empty group found"; + continue; + } + + // If not an object, complain and skip to the next one. + if (!iter.value().isObject()) + { + qWarning() << QString("Group '%1' in the group list should be an object").arg(groupName).toUtf8(); + continue; + } + + QJsonObject groupObj = iter.value().toObject(); + if (!groupObj.value("instances").isArray()) + { + qWarning() << QString( + "Group '%1' in the group list is invalid. It should contain an array called 'instances'.") + .arg(groupName) + .toUtf8(); + continue; + } + + auto hidden = groupObj.value("hidden").toBool(false); + if (hidden) + m_collapsedGroups.insert(groupName); + + // Iterate through the list of instances in the group. + QJsonArray instancesArray = groupObj.value("instances").toArray(); + + for (auto value : instancesArray) + { + m_instanceGroupIndex[value.toString()] = groupName; + increaseGroupCount(groupName); + } + } + + bool ungroupedHidden = false; + if (rootObj.value("ungrouped").isObject()) + { + QJsonObject ungrouped = rootObj.value("ungrouped").toObject(); + ungroupedHidden = ungrouped.value("hidden").toBool(false); + } + if (ungroupedHidden) + { + // empty string represents ungrouped "group" + m_collapsedGroups.insert(""); + } + m_groupsLoaded = true; + qDebug() << "Group list loaded."; +} + +void InstanceList::instanceDirContentsChanged(const QString& path) +{ + Q_UNUSED(path); + m_dirty = true; + if (m_watchLevel == 1) + { + m_reloadDebounceTimer.start(); + } +} + +void InstanceList::performDebouncedReload() +{ + if (m_watchLevel == 1 && m_dirty) + { + loadList(); + } +} + +void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting, QVariant value) +{ + QString newInstDir = QDir(value.toString()).canonicalPath(); + if (newInstDir != m_instDir) + { + if (m_groupsLoaded) + { + saveGroupList(); + } + m_instDir = newInstDir; + m_groupsLoaded = false; + beginRemoveRows(QModelIndex(), 0, count()); + m_instances.erase(m_instances.begin(), m_instances.end()); + endRemoveRows(); + emit instancesChanged(); + } +} + +void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) +{ + qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded"); + if (collapsed) + { + m_collapsedGroups.insert(group); + } + else + { + m_collapsedGroups.remove(group); + } + saveGroupList(); +} + +class InstanceStaging : public Task +{ + Q_OBJECT + const unsigned minBackoff = 1; + const unsigned maxBackoff = 16; + + public: + InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings) + : m_parent(parent), + backoff(minBackoff, maxBackoff) + { + m_stagingPath = parent->getStagedInstancePath(); + + m_child.reset(child); + + m_child->setStagingPath(m_stagingPath); + m_child->setParentSettings(std::move(settings)); + + connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); + connect(child, &Task::failed, this, &InstanceStaging::childFailed); + connect(child, &Task::aborted, this, &InstanceStaging::childAborted); + connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); + connect(child, &Task::abortButtonTextChanged, this, &InstanceStaging::setAbortButtonText); + connect(child, &Task::status, this, &InstanceStaging::setStatus); + connect(child, &Task::details, this, &InstanceStaging::setDetails); + connect(child, &Task::progress, this, &InstanceStaging::setProgress); + connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress); + connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); + } + + virtual ~InstanceStaging() + {} + + // Abort can now stop both the child task and any pending retries + bool abort() override + { + m_aborted = true; + m_backoffTimer.stop(); + + if (!m_child || !m_child->canAbort()) + return false; + + return m_child->abort(); + } + bool canAbort() const override { return (m_child && m_child->canAbort()); } + + protected: + virtual void executeTask() override + { + if (m_stagingPath.isNull()) + { + emitFailed(tr("Could not create staging folder")); + return; + } + + m_child->start(); + } + QStringList warnings() const override + { + return m_child->warnings(); + } + + private slots: + void childSucceeded() + { + unsigned sleepTime = backoff(); + if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) + { + emitSucceeded(); + return; + } + // we actually failed, retry? + if (sleepTime == maxBackoff) + { + emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); + return; + } + qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime; + m_backoffTimer.start(sleepTime * 500); + } + void childFailed(const QString& reason) + { + m_parent->destroyStagingPath(m_stagingPath); + emitFailed(reason); + } + + void childAborted() + { + m_parent->destroyStagingPath(m_stagingPath); + emitAborted(); + } + + private: + InstanceList* m_parent; + /* + * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. + * Basically, it starts messing things up while the launcher is extracting/creating instances + * and causes that horrible failure that is NTFS to lock files in place because they are open. + */ + ExponentialSeries backoff; + QString m_stagingPath; + unique_qobject_ptr m_child; + QTimer m_backoffTimer; + bool m_aborted = false; // Flag to track abort during backoff retries +}; + +Task* InstanceList::wrapInstanceTask(InstanceTask* task) +{ + return new InstanceStaging(this, task, m_globalSettings); +} + +QString InstanceList::getStagedInstancePath() +{ + const QString tempRoot = FS::PathCombine(m_instDir, ".tmp"); + + QString result; + int tries = 0; + + do + { + if (++tries > 256) + return {}; + + const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6); + result = FS::PathCombine(tempRoot, key); + } + while (QFileInfo::exists(result)); + + if (!QDir::current().mkpath(result)) + return {}; +#ifdef Q_OS_WIN32 + SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif + return result; +} + +bool InstanceList::commitStagedInstance(const QString& path, + InstanceName const& instanceName, + QString groupName, + InstanceTask const& commiting) +{ + if (groupName.isEmpty() && !groupName.isNull()) + groupName = QString(); + + QString instID; + InstancePtr inst; + + auto should_override = commiting.shouldOverride(); + + if (should_override) + { + instID = commiting.originalInstanceID(); + } + else + { + instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir); + } + + Q_ASSERT(!instID.isEmpty()); + + { + WatchLock lock(m_watcher, m_instDir); + QString destination = FS::PathCombine(m_instDir, instID); + + if (should_override) + { + if (!FS::overrideFolder(destination, path)) + { + qWarning() << "Failed to override" << path << "to" << destination; + return false; + } + } + else + { + if (!FS::move(path, destination)) + { + qWarning() << "Failed to move" << path << "to" << destination; + return false; + } + + m_instanceGroupIndex[instID] = groupName; + increaseGroupCount(groupName); + } + + instanceSet.insert(instID); + + emit instancesChanged(); + emit instanceSelectRequest(instID); + } + + saveGroupList(); + return true; +} + +bool InstanceList::destroyStagingPath(const QString& keyPath) +{ + return FS::deletePath(keyPath); +} + +int InstanceList::getTotalPlayTime() +{ + updateTotalPlayTime(); + return totalPlayTime; +} + +#include "InstanceList.moc" diff --git a/archived/projt-launcher/launcher/InstanceList.h b/archived/projt-launcher/launcher/InstanceList.h new file mode 100644 index 0000000000..f0d7ab71f5 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceList.h @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "BaseInstance.h" + +class QFileSystemWatcher; +class InstanceTask; +struct InstanceName; + +using InstanceId = QString; +using GroupId = QString; +using InstanceLocator = std::pair; + +enum class InstCreateError +{ + NoCreateError = 0, + NoSuchVersion, + UnknownCreateError, + InstExists, + CantCreateDir +}; + +enum class GroupsState +{ + NotLoaded, + Steady, + Dirty +}; + +struct TrashShortcutItem +{ + ShortcutData data; + QString trashPath; +}; + +struct TrashHistoryItem +{ + QString id; + QString path; + QString trashPath; + QString groupName; + QList shortcuts; +}; + +class InstanceList : public QAbstractListModel +{ + Q_OBJECT + + public: + explicit InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent = 0); + virtual ~InstanceList(); + + public: + QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + + enum AdditionalRoles + { + GroupRole = Qt::UserRole, + InstancePointerRole = 0x34B1CB48, ///< Return pointer to real instance + InstanceIDRole = 0x34B1CB49 ///< Return id if the instance + }; + /*! + * \brief Error codes returned by functions in the InstanceList class. + * NoError Indicates that no error occurred. + * UnknownError indicates that an unspecified error occurred. + */ + enum InstListError + { + NoError = 0, + UnknownError + }; + + InstancePtr at(int i) const + { + return m_instances.at(i); + } + + int count() const + { + return m_instances.count(); + } + + InstListError loadList(); + void saveNow(); + + /* O(n) */ + InstancePtr getInstanceById(QString id) const; + /* O(n) */ + InstancePtr getInstanceByManagedName(const QString& managed_name) const; + /* O(n) */ + QList getAllInstancesByManagedName(const QString& managed_name) const; + QModelIndex getInstanceIndexById(const QString& id) const; + QStringList getGroups(); + bool isGroupCollapsed(const QString& groupName); + + GroupId getInstanceGroup(const InstanceId& id) const; + void setInstanceGroup(const InstanceId& id, GroupId name); + + void deleteGroup(const GroupId& name); + void renameGroup(const GroupId& src, const GroupId& dst); + bool trashInstance(const InstanceId& id); + bool trashedSomething() const; + bool undoTrashInstance(); + void deleteInstance(const InstanceId& id); + + // Wrap an instance creation task in some more task machinery and make it ready to be used + Task* wrapInstanceTask(InstanceTask* task); + + /** + * Create a new empty staging area for instance creation and @return a path/key top commit it later. + * Used by instance manipulation tasks. + */ + QString getStagedInstancePath(); + + /** + * Commit the staging area given by @keyPath to the provider - used when creation succeeds. + * Used by instance manipulation tasks. + * should_override is used when another similar instance already exists, and we want to override it + * - for instance, when updating it. + */ + bool commitStagedInstance(const QString& keyPath, + const InstanceName& instanceName, + QString groupName, + const InstanceTask&); + + /** + * Destroy a previously created staging area given by @keyPath - used when creation fails. + * Used by instance manipulation tasks. + */ + bool destroyStagingPath(const QString& keyPath); + + int getTotalPlayTime(); + + Qt::DropActions supportedDragActions() const override; + + Qt::DropActions supportedDropActions() const override; + + bool canDropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) const override; + + bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) override; + + QStringList mimeTypes() const override; + QMimeData* mimeData(const QModelIndexList& indexes) const override; + + QStringList getLinkedInstancesById(const QString& id) const; + + signals: + void dataIsInvalid(); + void instancesChanged(); + void instanceSelectRequest(QString instanceId); + void groupsChanged(QSet groups); + + public slots: + void on_InstFolderChanged(const Setting& setting, QVariant value); + void on_GroupStateChanged(const QString& group, bool collapsed); + + private slots: + void propertiesChanged(BaseInstance* inst); + void providerUpdated(); + void instanceDirContentsChanged(const QString& path); + void performDebouncedReload(); + + private: + int getInstIndex(BaseInstance* inst) const; + void updateTotalPlayTime(); + void suspendWatch(); + void resumeWatch(); + void add(const QList& list); + void loadGroupList(); + void saveGroupList(); + QList discoverInstances(); + InstancePtr loadInstance(const InstanceId& id); + + void increaseGroupCount(const QString& group); + void decreaseGroupCount(const QString& group); + + /// Removes instances that no longer exist on disk, optimizing for contiguous removals. + /// @param deadInstances Map of instance IDs to their (pointer, index) pairs to be removed. + void removeDeadInstances(const QMap& deadInstances); + + private: + int m_watchLevel = 0; + int totalPlayTime = 0; + bool m_dirty = false; + QList m_instances; + // id -> refs + QMap m_groupNameCache; + + SettingsObjectPtr m_globalSettings; + QString m_instDir; + QFileSystemWatcher* m_watcher; + // NOTE: Optimized using QHash for cached lookups. + QSet m_collapsedGroups; + QHash m_instanceGroupIndex; + QSet instanceSet; + QHash m_instanceMap; + bool m_groupsLoaded = false; + bool m_instancesProbed = false; + QTimer m_reloadDebounceTimer; + + QStack m_trashHistory; +}; diff --git a/archived/projt-launcher/launcher/InstancePageProvider.h b/archived/projt-launcher/launcher/InstancePageProvider.h new file mode 100644 index 0000000000..18808f1c13 --- /dev/null +++ b/archived/projt-launcher/launcher/InstancePageProvider.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" +#include "ui/pages/BasePageProvider.h" +#include "ui/pages/instance/BackupPage.h" +#include "ui/pages/instance/InstanceSettingsPage.h" +#include "ui/pages/instance/LogPage.h" +#include "ui/pages/instance/ManagedPackPage.h" +#include "ui/pages/instance/ModFolderPage.h" +#include "ui/pages/instance/NotesPage.h" +#include "ui/pages/instance/OtherLogsPage.h" +#include "ui/pages/instance/ResourcePackPage.h" +#include "ui/pages/instance/ScreenshotsPage.h" +#include "ui/pages/instance/ServersPage.h" +#include "ui/pages/instance/ShaderPackPage.h" +#include "ui/pages/instance/TexturePackPage.h" +#include "ui/pages/instance/VersionPage.h" +#include "ui/pages/instance/WorldListPage.h" + +class InstancePageProvider : protected QObject, public BasePageProvider +{ + Q_OBJECT + public: + explicit InstancePageProvider(InstancePtr parent) + { + inst = parent; + } + + virtual ~InstancePageProvider() = default; + virtual QList getPages() override + { + QList values; + values.append(new LogPage(inst)); + std::shared_ptr onesix = std::dynamic_pointer_cast(inst); + values.append(new VersionPage(onesix.get())); + values.append(ManagedPackPage::createPage(onesix.get())); + auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); + modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)"); + values.append(modsPage); + values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); + values.append(new NilModFolderPage(onesix.get(), onesix->nilModList())); + values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); + values.append(new GlobalDataPackPage(onesix.get())); + values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); + values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); + values.append(new NotesPage(onesix.get())); + values.append(new WorldListPage(onesix, onesix->worldList())); + values.append(new ServersPage(onesix)); + values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); + values.append(new BackupPage(onesix.get())); + values.append(new InstanceSettingsPage(onesix)); + values.append(new OtherLogsPage("logs", tr("Other Logs"), "Other-Logs", inst)); + return values; + } + + virtual QString dialogTitle() override + { + return tr("Edit Instance (%1)").arg(inst->name()); + } + + protected: + InstancePtr inst; +}; diff --git a/archived/projt-launcher/launcher/InstanceTask.cpp b/archived/projt-launcher/launcher/InstanceTask.cpp new file mode 100644 index 0000000000..e836b396b9 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceTask.cpp @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "InstanceTask.h" + +#include "Application.h" +#include "settings/SettingsObject.h" +#include "ui/dialogs/CustomMessageBox.h" + +#include + +InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) +{ + auto dialog = CustomMessageBox::selectable( + parent, + QObject::tr("Change instance name"), + QObject::tr("The instance's name seems to include the old version. Would you like to update it?\n\n" + "Old name: %1\n" + "New name: %2") + .arg(old_name, new_name), + QMessageBox::Question, + QMessageBox::No | QMessageBox::Yes); + auto result = dialog->exec(); + + if (result == QMessageBox::Yes) + return InstanceNameChange::ShouldChange; + return InstanceNameChange::ShouldKeep; +} + +ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name) +{ + if (APPLICATION->settings()->get("SkipModpackUpdatePrompt").toBool()) + return ShouldUpdate::SkipUpdating; + + auto info = CustomMessageBox::selectable( + parent, + QObject::tr("Similar modpack was found!"), + QObject::tr("One or more of your instances are from this same modpack%1. Do you want to create a " + "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your " + "important instance data before " + "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") + .arg(original_version_name), + QMessageBox::Information, + QMessageBox::Cancel); + QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole); + QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole); + + info->exec(); + + if (info->clickedButton() == update) + return ShouldUpdate::Update; + if (info->clickedButton() == skip) + return ShouldUpdate::SkipUpdating; + return ShouldUpdate::Cancel; +} + +QString InstanceName::name() const +{ + if (!m_modified_name.isEmpty()) + return modifiedName(); + if (!m_original_version.isEmpty()) + return QString("%1 %2").arg(m_original_name, m_original_version); + + return m_original_name; +} + +QString InstanceName::originalName() const +{ + return m_original_name; +} + +QString InstanceName::modifiedName() const +{ + if (!m_modified_name.isEmpty()) + return m_modified_name; + return m_original_name; +} + +QString InstanceName::version() const +{ + return m_original_version; +} + +void InstanceName::setName(InstanceName& other) +{ + m_original_name = other.m_original_name; + m_original_version = other.m_original_version; + m_modified_name = other.m_modified_name; +} + +InstanceTask::InstanceTask() : Task(), InstanceName() +{} diff --git a/archived/projt-launcher/launcher/InstanceTask.h b/archived/projt-launcher/launcher/InstanceTask.h new file mode 100644 index 0000000000..f7647e2448 --- /dev/null +++ b/archived/projt-launcher/launcher/InstanceTask.h @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "settings/SettingsObject.h" +#include "tasks/Task.h" + +/* Helpers */ +enum class InstanceNameChange +{ + ShouldChange, + ShouldKeep +}; +[[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, + const QString& old_name, + const QString& new_name); +enum class ShouldUpdate +{ + Update, + SkipUpdating, + Cancel +}; +[[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name); + +struct InstanceName +{ + public: + InstanceName() = default; + InstanceName(QString name, QString version) + : m_original_name(std::move(name)), + m_original_version(std::move(version)) + {} + + QString modifiedName() const; + QString originalName() const; + QString name() const; + QString version() const; + + void setName(QString name) + { + m_modified_name = name; + } + void setName(InstanceName& other); + + protected: + QString m_original_name; + QString m_original_version; + + QString m_modified_name; +}; + +class InstanceTask : public Task, public InstanceName +{ + Q_OBJECT + public: + InstanceTask(); + ~InstanceTask() override = default; + + void setParentSettings(SettingsObjectPtr settings) + { + m_globalSettings = settings; + } + + void setStagingPath(const QString& stagingPath) + { + m_stagingPath = stagingPath; + } + + void setIcon(const QString& icon) + { + m_instIcon = icon; + } + + void setGroup(const QString& group) + { + m_instGroup = group; + } + QString group() const + { + return m_instGroup; + } + + bool shouldConfirmUpdate() const + { + return m_confirm_update; + } + void setConfirmUpdate(bool confirm) + { + m_confirm_update = confirm; + } + + bool shouldOverride() const + { + return m_override_existing; + } + + QString originalInstanceID() const + { + return m_original_instance_id; + }; + + protected: + void setOverride(bool override, QString instance_id_to_override = {}) + { + m_override_existing = override; + if (!instance_id_to_override.isEmpty()) + m_original_instance_id = instance_id_to_override; + } + + protected: /* data */ + SettingsObjectPtr m_globalSettings; + QString m_instIcon; + QString m_instGroup; + QString m_stagingPath; + + bool m_override_existing = false; + bool m_confirm_update = true; + + QString m_original_instance_id; +}; diff --git a/archived/projt-launcher/launcher/JavaCommon.cpp b/archived/projt-launcher/launcher/JavaCommon.cpp new file mode 100644 index 0000000000..5986249178 --- /dev/null +++ b/archived/projt-launcher/launcher/JavaCommon.cpp @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "JavaCommon.h" +#include "java/services/RuntimeProbeTask.hpp" +#include "ui/dialogs/CustomMessageBox.h" + +#include + +bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) +{ + static const QRegularExpression s_memRegex("-Xm[sx]"); + static const QRegularExpression s_versionRegex("-version:.*"); + if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") + || jvmargs.contains("-XX:InitialHeapSize")) + { + auto warnStr = QObject::tr( + "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", " + "\"-XX:InitialHeapSize\", \"-Xmx\" " + "or \"-Xms\").\n" + "There are dedicated boxes for these in the settings (Java tab, in the Memory group at the top).\n" + "This message will be displayed until you remove them from the JVM arguments."); + CustomMessageBox::selectable(parent, QObject::tr("JVM arguments warning"), warnStr, QMessageBox::Warning) + ->exec(); + return false; + } + // block lunacy with passing required version to the JVM + if (jvmargs.contains(s_versionRegex)) + { + auto warnStr = QObject::tr("You tried to pass required Java version argument to the JVM (using " + "\"-version:xxx\"). This is not safe and will not be " + "allowed.\n" + "This message will be displayed until you remove this from the JVM arguments."); + CustomMessageBox::selectable(parent, QObject::tr("JVM arguments warning"), warnStr, QMessageBox::Warning) + ->exec(); + return false; + } + return true; +} + +void JavaCommon::javaWasOk(QWidget* parent, const projt::java::RuntimeProbeTask::ProbeReport& result) +{ + QString text; + text += QObject::tr("Java test succeeded!
Platform reported: %1
Java version " + "reported: %2
Java vendor " + "reported: %3
") + .arg(result.platformArch, result.version.toString(), result.vendor); + if (result.stderrLog.size()) + { + auto htmlError = result.stderrLog; + htmlError.replace('\n', "
"); + text += QObject::tr("
Warnings:
%1").arg(htmlError); + } + CustomMessageBox::selectable(parent, QObject::tr("Java test success"), text, QMessageBox::Information)->show(); +} + +void JavaCommon::javaArgsWereBad(QWidget* parent, const projt::java::RuntimeProbeTask::ProbeReport& result) +{ + auto htmlError = result.stderrLog; + QString text; + htmlError.replace('\n', "
"); + text += QObject::tr("The specified Java binary didn't work with the arguments you provided:
"); + text += QString("%1").arg(htmlError); + CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); +} + +void JavaCommon::javaBinaryWasBad(QWidget* parent, const projt::java::RuntimeProbeTask::ProbeReport& result) +{ + QString text; + text += QObject::tr("The specified Java binary didn't work.
You should press 'Detect', " + "or set the path to the Java executable.
"); + CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); +} + +void JavaCommon::javaCheckNotFound(QWidget* parent) +{ + QString text; + text += QObject::tr("Java checker library could not be found. Please check your installation."); + CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); +} + +void JavaCommon::TestCheck::run() +{ + if (!JavaCommon::checkJVMArgs(m_args, m_parent)) + { + emit finished(); + return; + } + if (projt::java::RuntimeProbeTask::probeJarPath().isEmpty()) + { + javaCheckNotFound(m_parent); + emit finished(); + return; + } + projt::java::RuntimeProbeTask::ProbeSettings settings; + settings.binaryPath = m_path; + checker.reset(new projt::java::RuntimeProbeTask(settings)); + connect(checker.get(), &projt::java::RuntimeProbeTask::probeFinished, this, &JavaCommon::TestCheck::checkFinished); + checker->start(); +} + +void JavaCommon::TestCheck::checkFinished(const projt::java::RuntimeProbeTask::ProbeReport& result) +{ + if (result.status != projt::java::RuntimeProbeTask::ProbeReport::Status::Valid) + { + javaBinaryWasBad(m_parent, result); + emit finished(); + return; + } + projt::java::RuntimeProbeTask::ProbeSettings settings; + settings.binaryPath = m_path; + settings.extraArgs = m_args; + settings.minMem = m_minMem; + settings.maxMem = m_maxMem; + settings.permGen = result.version.needsPermGen() ? m_permGen : 0; + checker.reset(new projt::java::RuntimeProbeTask(settings)); + connect(checker.get(), + &projt::java::RuntimeProbeTask::probeFinished, + this, + &JavaCommon::TestCheck::checkFinishedWithArgs); + checker->start(); +} + +void JavaCommon::TestCheck::checkFinishedWithArgs(const projt::java::RuntimeProbeTask::ProbeReport& result) +{ + if (result.status == projt::java::RuntimeProbeTask::ProbeReport::Status::Valid) + { + javaWasOk(m_parent, result); + emit finished(); + return; + } + javaArgsWereBad(m_parent, result); + emit finished(); +} diff --git a/archived/projt-launcher/launcher/JavaCommon.h b/archived/projt-launcher/launcher/JavaCommon.h new file mode 100644 index 0000000000..6193eccdde --- /dev/null +++ b/archived/projt-launcher/launcher/JavaCommon.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include + +class QWidget; + +/** + * Common UI bits for the java pages to use. + */ +namespace JavaCommon +{ + bool checkJVMArgs(QString args, QWidget* parent); + + // Show a dialog saying that the Java binary was usable + void javaWasOk(QWidget* parent, const projt::java::RuntimeProbeTask::ProbeReport& result); + // Show a dialog saying that the Java binary was not usable because of bad options + void javaArgsWereBad(QWidget* parent, const projt::java::RuntimeProbeTask::ProbeReport& result); + // Show a dialog saying that the Java binary was not usable + void javaBinaryWasBad(QWidget* parent, const projt::java::RuntimeProbeTask::ProbeReport& result); + // Show a dialog if we couldn't find Java Checker + void javaCheckNotFound(QWidget* parent); + + class TestCheck : public QObject + { + Q_OBJECT + public: + TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) + : m_parent(parent), + m_path(path), + m_args(args), + m_minMem(minMem), + m_maxMem(maxMem), + m_permGen(permGen) + {} + virtual ~TestCheck() = default; + + void run(); + + signals: + void finished(); + + private slots: + void checkFinished(const projt::java::RuntimeProbeTask::ProbeReport& result); + void checkFinishedWithArgs(const projt::java::RuntimeProbeTask::ProbeReport& result); + + private: + projt::java::RuntimeProbeTask::Ptr checker; + QWidget* m_parent = nullptr; + QString m_path; + QString m_args; + int m_minMem = 0; + int m_maxMem = 0; + int m_permGen = 64; + }; +} // namespace JavaCommon diff --git a/archived/projt-launcher/launcher/Json.cpp b/archived/projt-launcher/launcher/Json.cpp new file mode 100644 index 0000000000..27393afc8c --- /dev/null +++ b/archived/projt-launcher/launcher/Json.cpp @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include "Json.h" + +#include + +#include +#include "FileSystem.h" + +namespace Json +{ + void write(const QJsonDocument& doc, const QString& filename) + { + FS::write(filename, doc.toJson()); + } + void write(const QJsonObject& object, const QString& filename) + { + write(QJsonDocument(object), filename); + } + void write(const QJsonArray& array, const QString& filename) + { + write(QJsonDocument(array), filename); + } + + QByteArray toText(const QJsonObject& obj) + { + return QJsonDocument(obj).toJson(QJsonDocument::Compact); + } + QByteArray toText(const QJsonArray& array) + { + return QJsonDocument(array).toJson(QJsonDocument::Compact); + } + + static bool isBinaryJson(const QByteArray& data) + { + decltype(QJsonDocument::BinaryFormatTag) tag = QJsonDocument::BinaryFormatTag; + return memcmp(data.constData(), &tag, sizeof(QJsonDocument::BinaryFormatTag)) == 0; + } + QJsonDocument requireDocument(const QByteArray& data, const QString& what) + { + if (isBinaryJson(data)) + { + // Binary JSON is not supported by this application + throw JsonException(what + ": Invalid JSON. Binary JSON unsupported"); + } + else + { + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) + { + throw JsonException(what + ": Error parsing JSON: " + error.errorString()); + } + return doc; + } + } + QJsonDocument requireDocument(const QString& filename, const QString& what) + { + return requireDocument(FS::read(filename), what); + } + QJsonObject requireObject(const QJsonDocument& doc, const QString& what) + { + if (!doc.isObject()) + { + throw JsonException(what + " is not an object"); + } + return doc.object(); + } + QJsonArray requireArray(const QJsonDocument& doc, const QString& what) + { + if (!doc.isArray()) + { + throw JsonException(what + " is not an array"); + } + return doc.array(); + } + + void writeString(QJsonObject& to, const QString& key, const QString& value) + { + if (!value.isEmpty()) + { + to.insert(key, value); + } + } + + void writeStringList(QJsonObject& to, const QString& key, const QStringList& values) + { + if (!values.isEmpty()) + { + QJsonArray array; + for (auto value : values) + { + array.append(value); + } + to.insert(key, array); + } + } + + template <> + QJsonValue toJson(const QUrl& url) + { + return QJsonValue(url.toString(QUrl::FullyEncoded)); + } + template <> + QJsonValue toJson(const QByteArray& data) + { + return QJsonValue(QString::fromLatin1(data.toHex())); + } + template <> + QJsonValue toJson(const QDateTime& datetime) + { + return QJsonValue(datetime.toString(Qt::ISODate)); + } + template <> + QJsonValue toJson(const QDir& dir) + { + return QDir::current().relativeFilePath(dir.absolutePath()); + } + template <> + QJsonValue toJson(const QUuid& uuid) + { + return uuid.toString(); + } + template <> + QJsonValue toJson(const QVariant& variant) + { + return QJsonValue::fromVariant(variant); + } + + template <> + QByteArray requireIsType(const QJsonValue& value, const QString& what) + { + const QString string = ensureIsType(value, what); + // ensure that the string can be safely cast to Latin1 + if (string != QString::fromLatin1(string.toLatin1())) + { + throw JsonException(what + " is not encodable as Latin1"); + } + return QByteArray::fromHex(string.toLatin1()); + } + + template <> + QJsonArray requireIsType(const QJsonValue& value, const QString& what) + { + if (!value.isArray()) + { + throw JsonException(what + " is not an array"); + } + return value.toArray(); + } + + template <> + QString requireIsType(const QJsonValue& value, const QString& what) + { + if (!value.isString()) + { + throw JsonException(what + " is not a string"); + } + return value.toString(); + } + + template <> + bool requireIsType(const QJsonValue& value, const QString& what) + { + if (!value.isBool()) + { + throw JsonException(what + " is not a bool"); + } + return value.toBool(); + } + + template <> + double requireIsType(const QJsonValue& value, const QString& what) + { + if (!value.isDouble()) + { + throw JsonException(what + " is not a double"); + } + // Burada value'nin double'a çevrilmesi bekleniyor. EÄŸer value uygun deÄŸilse, Qt varsayılan olarak 0 döndürür. + return value.toDouble(); + } + + template <> + int requireIsType(const QJsonValue& value, const QString& what) + { + const double doubl = requireIsType(value, what); + if (fmod(doubl, 1) != 0) + { + throw JsonException(what + " is not an integer"); + } + return int(doubl); + } + + template <> + QDateTime requireIsType(const QJsonValue& value, const QString& what) + { + const QString string = requireIsType(value, what); + const QDateTime datetime = QDateTime::fromString(string, Qt::ISODate); + if (!datetime.isValid()) + { + throw JsonException(what + " is not a ISO formatted date/time value"); + } + return datetime; + } + + template <> + QUrl requireIsType(const QJsonValue& value, const QString& what) + { + const QString string = ensureIsType(value, what); + if (string.isEmpty()) + { + return QUrl(); + } + const QUrl url = QUrl(string, QUrl::StrictMode); + if (!url.isValid()) + { + throw JsonException(what + " is not a correctly formatted URL"); + } + return url; + } + + template <> + QDir requireIsType(const QJsonValue& value, const QString& what) + { + const QString string = requireIsType(value, what); + + // Security: Prevent path traversal attacks + if (QFileInfo(string).isAbsolute()) + { + throw JsonException(what + ": Absolute paths are not allowed"); + } + + // Sanitize path - remove dangerous characters and sequences + QString sanitized = string; + sanitized.replace("..", ""); // Remove parent directory references + sanitized.replace("\\", "/"); // Normalize separators + // Remove other potentially dangerous characters + sanitized.remove(QRegularExpression("[<>:\"|?*]")); + + return QDir::current().absoluteFilePath(sanitized); + } + + template <> + QUuid requireIsType(const QJsonValue& value, const QString& what) + { + const QString string = requireIsType(value, what); + const QUuid uuid = QUuid(string); + if (uuid.toString() != string) // converts back => valid + { + throw JsonException(what + " is not a valid UUID"); + } + return uuid; + } + + template <> + QJsonObject requireIsType(const QJsonValue& value, const QString& what) + { + if (!value.isObject()) + { + throw JsonException(what + " is not an object"); + } + return value.toObject(); + } + + template <> + QVariant requireIsType(const QJsonValue& value, const QString& what) + { + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value.toVariant(); + } + + template <> + QJsonValue requireIsType(const QJsonValue& value, const QString& what) + { + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value; + } + + QStringList toStringList(const QString& jsonString) + { + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isArray()) + return {}; + try + { + return ensureIsArrayOf(doc.array(), ""); + } + catch (Json::JsonException&) + { + return {}; + } + } + + QString fromStringList(const QStringList& list) + { + QJsonArray array; + for (const QString& str : list) + { + array.append(str); + } + + QJsonDocument doc(toJsonArray(list)); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); + } + + QVariantMap toMap(const QString& jsonString) + { + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) + return {}; + + QJsonObject obj = doc.object(); + return obj.toVariantMap(); + } + + QString fromMap(const QVariantMap& map) + { + QJsonObject obj = QJsonObject::fromVariantMap(map); + QJsonDocument doc(obj); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); + } + +} // namespace Json diff --git a/archived/projt-launcher/launcher/Json.h b/archived/projt-launcher/launcher/Json.h new file mode 100644 index 0000000000..2b738f3f74 --- /dev/null +++ b/archived/projt-launcher/launcher/Json.h @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Exception.h" + +namespace Json +{ + class JsonException : public ::Exception + { + public: + JsonException(const QString& message) : Exception(message) + {} + }; + + /// @throw FileSystemException + void write(const QJsonDocument& doc, const QString& filename); + /// @throw FileSystemException + void write(const QJsonObject& object, const QString& filename); + /// @throw FileSystemException + void write(const QJsonArray& array, const QString& filename); + + QByteArray toText(const QJsonObject& obj); + QByteArray toText(const QJsonArray& array); + + /// @throw JsonException + QJsonDocument requireDocument(const QByteArray& data, const QString& what = "Document"); + /// @throw JsonException + QJsonDocument requireDocument(const QString& filename, const QString& what = "Document"); + /// @throw JsonException + QJsonObject requireObject(const QJsonDocument& doc, const QString& what = "Document"); + /// @throw JsonException + QJsonArray requireArray(const QJsonDocument& doc, const QString& what = "Document"); + + /////////////////// WRITING //////////////////// + + void writeString(QJsonObject& to, const QString& key, const QString& value); + void writeStringList(QJsonObject& to, const QString& key, const QStringList& values); + + template + QJsonValue toJson(const T& t) + { + return QJsonValue(t); + } + template <> + QJsonValue toJson(const QUrl& url); + template <> + QJsonValue toJson(const QByteArray& data); + template <> + QJsonValue toJson(const QDateTime& datetime); + template <> + QJsonValue toJson(const QDir& dir); + template <> + QJsonValue toJson(const QUuid& uuid); + template <> + QJsonValue toJson(const QVariant& variant); + + template + QJsonArray toJsonArray(const QList& container) + { + QJsonArray array; + for (const T& item : container) + { + array.append(toJson(item)); + } + return array; + } + + ////////////////// READING //////////////////// + + /// @throw JsonException + template + T requireIsType(const QJsonValue& value, const QString& what = "Value"); + + /// @throw JsonException + template <> + double requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + bool requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + int requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QJsonObject requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QJsonArray requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QJsonValue requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QByteArray requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QDateTime requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QVariant requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QString requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QUuid requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QDir requireIsType(const QJsonValue& value, const QString& what); + /// @throw JsonException + template <> + QUrl requireIsType(const QJsonValue& value, const QString& what); + + // the following functions are higher level functions, that make use of the above functions for + // type conversion + template + T ensureIsType(const QJsonValue& value, const T default_ = T(), const QString& what = "Value") + { + if (value.isUndefined() || value.isNull()) + { + return default_; + } + try + { + return requireIsType(value, what); + } + catch (const JsonException&) + { + return default_; + } + } + + /// @throw JsonException + template + T requireIsType(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") + { + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + throw JsonException(localWhat + "s parent does not contain " + localWhat); + } + return requireIsType(parent.value(key), localWhat); + } + + template + T ensureIsType(const QJsonObject& parent, + const QString& key, + const T default_ = T(), + const QString& what = "__placeholder__") + { + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsType(parent.value(key), default_, localWhat); + } + + template + QList requireIsArrayOf(const QJsonDocument& doc) + { + const QJsonArray array = requireArray(doc); + QList out; + for (const QJsonValue val : array) + { + out.append(requireIsType(val, "Document")); + } + return out; + } + + template + QList ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") + { + const QJsonArray array = ensureIsType(value, QJsonArray(), what); + QList out; + for (const QJsonValue val : array) + { + out.append(requireIsType(val, what)); + } + return out; + } + + template + QList ensureIsArrayOf(const QJsonValue& value, const QList default_, const QString& what = "Value") + { + if (value.isUndefined()) + { + return default_; + } + return ensureIsArrayOf(value, what); + } + + /// @throw JsonException + template + QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") + { + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + throw JsonException(localWhat + "s parent does not contain " + localWhat); + } + return ensureIsArrayOf(parent.value(key), localWhat); + } + + template + QList ensureIsArrayOf(const QJsonObject& parent, + const QString& key, + const QList& default_ = QList(), + const QString& what = "__placeholder__") + { + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsArrayOf(parent.value(key), default_, localWhat); + } + +// this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work +// well with IDE helpers +#define JSON_HELPERFUNCTIONS(NAME, TYPE) \ + inline TYPE require##NAME(const QJsonValue& value, const QString& what = "Value") \ + { \ + return requireIsType(value, what); \ + } \ + inline TYPE ensure##NAME(const QJsonValue& value, const TYPE default_ = TYPE(), const QString& what = "Value") \ + { \ + return ensureIsType(value, default_, what); \ + } \ + inline TYPE require##NAME(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") \ + { \ + return requireIsType(parent, key, what); \ + } \ + inline TYPE ensure##NAME(const QJsonObject& parent, \ + const QString& key, \ + const TYPE default_ = TYPE(), \ + const QString& what = "__placeholder") \ + { \ + return ensureIsType(parent, key, default_, what); \ + } + + JSON_HELPERFUNCTIONS(Array, QJsonArray) + JSON_HELPERFUNCTIONS(Object, QJsonObject) + JSON_HELPERFUNCTIONS(JsonValue, QJsonValue) + JSON_HELPERFUNCTIONS(String, QString) + JSON_HELPERFUNCTIONS(Boolean, bool) + JSON_HELPERFUNCTIONS(Double, double) + JSON_HELPERFUNCTIONS(Integer, int) + JSON_HELPERFUNCTIONS(DateTime, QDateTime) + JSON_HELPERFUNCTIONS(Url, QUrl) + JSON_HELPERFUNCTIONS(ByteArray, QByteArray) + JSON_HELPERFUNCTIONS(Dir, QDir) + JSON_HELPERFUNCTIONS(Uuid, QUuid) + JSON_HELPERFUNCTIONS(Variant, QVariant) + +#undef JSON_HELPERFUNCTIONS + + // helper functions for settings + QStringList toStringList(const QString& jsonString); + QString fromStringList(const QStringList& list); + + QVariantMap toMap(const QString& jsonString); + QString fromMap(const QVariantMap& map); + +} // namespace Json +using JSONValidationError = Json::JsonException; diff --git a/archived/projt-launcher/launcher/Kconfig b/archived/projt-launcher/launcher/Kconfig new file mode 100644 index 0000000000..446639f255 --- /dev/null +++ b/archived/projt-launcher/launcher/Kconfig @@ -0,0 +1,287 @@ +# launcher Kconfig - Main launcher application +# Included from main Kconfig + +menuconfig LAUNCHER + bool "Main launcher application" + default y + help + The ProjT Launcher main application. + +if LAUNCHER + +menu "Core Components" + +config LAUNCHER_CORE + bool "Core functionality" + default y + help + Core launcher functionality (required). + +config LAUNCHER_GUI + bool "GUI (Qt Widgets)" + default y + help + Qt-based graphical user interface. + +config LAUNCHER_CLI + bool "Command-line interface" + default n + help + Command-line interface for headless operation. + +endmenu + +menu "Instance Management" + +config LAUNCHER_INSTANCES + bool "Instance management" + default y + help + Minecraft instance creation and management. + +config LAUNCHER_INSTANCE_COPY + bool "Instance copy/clone" + default y + depends on LAUNCHER_INSTANCES + +config LAUNCHER_INSTANCE_EXPORT + bool "Instance export" + default y + depends on LAUNCHER_INSTANCES + help + Export instances as archives. + +config LAUNCHER_INSTANCE_IMPORT + bool "Instance import" + default y + depends on LAUNCHER_INSTANCES + help + Import instances from archives. + +config LAUNCHER_INSTANCE_SYMLINKS + bool "Linked instances" + default y + depends on LAUNCHER_INSTANCES + help + Support for linked instances (shared files). + +endmenu + +menu "Minecraft Support" + +config LAUNCHER_MINECRAFT_VANILLA + bool "Vanilla Minecraft" + default y + help + Support for vanilla Minecraft versions. + +config LAUNCHER_MINECRAFT_FORGE + bool "Forge support" + default y + help + Support for Minecraft Forge modloader. + +config LAUNCHER_MINECRAFT_FABRIC + bool "Fabric support" + default y + help + Support for Fabric modloader. + +config LAUNCHER_MINECRAFT_QUILT + bool "Quilt support" + default y + help + Support for Quilt modloader. + +config LAUNCHER_MINECRAFT_NEOFORGE + bool "NeoForge support" + default y + help + Support for NeoForge modloader. + +config LAUNCHER_MINECRAFT_LITELOADER + bool "LiteLoader support" + default y + help + Support for LiteLoader (legacy). + +endmenu + +menu "Mod Platforms" + +config LAUNCHER_MODRINTH + bool "Modrinth integration" + default y + help + Modrinth mod/modpack platform support. + +config LAUNCHER_CURSEFORGE + bool "CurseForge/Flame integration" + default y + help + CurseForge mod/modpack platform support. + +config LAUNCHER_TECHNIC + bool "Technic integration" + default y + help + Technic platform modpack support. + +config LAUNCHER_ATLAUNCHER + bool "ATLauncher integration" + default y + help + ATLauncher modpack support. + +config LAUNCHER_FTB + bool "Feed The Beast integration" + default y + help + FTB modpack support. + +endmenu + +menu "Authentication" + +config LAUNCHER_AUTH_MSA + bool "Microsoft Account login" + default y + help + Microsoft/Xbox Live authentication. + +config LAUNCHER_AUTH_OFFLINE + bool "Offline mode" + default y + help + Allow offline play without authentication. + +config LAUNCHER_AUTH_ELY_BY + bool "Ely.by authentication" + default n + help + Ely.by authentication service support. + +endmenu + +menu "UI Features" + +config LAUNCHER_THEMES + bool "Custom themes" + default y + help + User-customizable themes. + +config LAUNCHER_ICONS + bool "Custom icon packs" + default y + help + Custom instance icon support. + +config LAUNCHER_SCREENSHOTS + bool "Screenshot viewer" + default y + help + In-app screenshot viewing and management. + +config LAUNCHER_LOGS + bool "Log viewer" + default y + help + In-app log viewing and upload. + +config LAUNCHER_CONSOLE + bool "Console output" + default y + help + Game console output display. + +config LAUNCHER_NEWS + bool "News feed" + default y + help + News feed from project website. + +config LAUNCHER_CATS + bool "Cat mode" + default y + help + Essential cat-related functionality. + +endmenu + +menu "Java Management" + +config LAUNCHER_JAVA_DETECT + bool "Java detection" + default y + help + Automatic Java installation detection. + +config LAUNCHER_JAVA_DOWNLOAD + bool "Java auto-download" + default y + help + Automatic Java download and installation. + +config LAUNCHER_JAVA_ADOPTIUM + bool "Adoptium Temurin" + default y + depends on LAUNCHER_JAVA_DOWNLOAD + +config LAUNCHER_JAVA_AZUL + bool "Azul Zulu" + default y + depends on LAUNCHER_JAVA_DOWNLOAD + +config LAUNCHER_JAVA_MICROSOFT + bool "Microsoft OpenJDK" + default y + depends on LAUNCHER_JAVA_DOWNLOAD + +endmenu + +menu "Updates" + +config LAUNCHER_UPDATER + bool "Auto-updater" + default y + help + In-app self-update functionality. + +config LAUNCHER_UPDATER_GITHUB + bool "GitHub releases" + default y + depends on LAUNCHER_UPDATER + +config LAUNCHER_UPDATER_SPARKLE + bool "Sparkle (macOS)" + default y + depends on LAUNCHER_UPDATER && TARGET_MACOS + +endmenu + +menu "Integrations" + +config LAUNCHER_DISCORD_RPC + bool "Discord Rich Presence" + default y + help + Show game status in Discord. + +config LAUNCHER_GAMEMODE + bool "GameMode integration" + default y + depends on TARGET_LINUX || TARGET_AUTO + help + Feral GameMode performance optimization. + +config LAUNCHER_MANGOHUD + bool "MangoHud integration" + default y + depends on TARGET_LINUX || TARGET_AUTO + help + MangoHud performance overlay support. + +endmenu + +endif # LAUNCHER diff --git a/archived/projt-launcher/launcher/KonamiCode.cpp b/archived/projt-launcher/launcher/KonamiCode.cpp new file mode 100644 index 0000000000..0fd38a0978 --- /dev/null +++ b/archived/projt-launcher/launcher/KonamiCode.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "KonamiCode.h" + +#include +#include + +namespace +{ + const std::array konamiCode = { { Qt::Key_Up, + Qt::Key_Up, + Qt::Key_Down, + Qt::Key_Down, + Qt::Key_Left, + Qt::Key_Right, + Qt::Key_Left, + Qt::Key_Right, + Qt::Key_B, + Qt::Key_A } }; +} + +KonamiCode::KonamiCode(QObject* parent) : QObject(parent) +{} + +void KonamiCode::input(QEvent* event) +{ + if (event->type() == QEvent::KeyPress) + { + QKeyEvent* keyEvent = static_cast(event); + auto key = Qt::Key(keyEvent->key()); + if (key == konamiCode[m_progress]) + { + m_progress++; + } + else + { + m_progress = 0; + } + if (m_progress == static_cast(konamiCode.size())) + { + m_progress = 0; + emit triggered(); + } + } +} diff --git a/archived/projt-launcher/launcher/KonamiCode.h b/archived/projt-launcher/launcher/KonamiCode.h new file mode 100644 index 0000000000..41f7d618ef --- /dev/null +++ b/archived/projt-launcher/launcher/KonamiCode.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +class KonamiCode : public QObject +{ + Q_OBJECT + public: + KonamiCode(QObject* parent = 0); + void input(QEvent* event); + + signals: + void triggered(); + + private: + int m_progress = 0; +}; diff --git a/archived/projt-launcher/launcher/LaunchController.cpp b/archived/projt-launcher/launcher/LaunchController.cpp new file mode 100644 index 0000000000..fa43307ef8 --- /dev/null +++ b/archived/projt-launcher/launcher/LaunchController.cpp @@ -0,0 +1,601 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "LaunchController.h" + +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "BuildConfig.h" +#include "JavaCommon.h" +#include "launch/LaunchPipeline.hpp" +#include "launch/steps/HostLookupReportStep.hpp" +#include "launch/steps/LogMessageStep.hpp" +#include "minecraft/auth/AccountData.hpp" +#include "minecraft/auth/AccountList.hpp" +#include "tasks/Task.h" +#include "ui/InstanceWindow.h" +#include "ui/MainWindow.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/MSALoginDialog.h" +#include "ui/dialogs/ProfileSelectDialog.h" +#include "ui/dialogs/ProfileSetupDialog.h" +#include "ui/dialogs/ProgressDialog.h" + +namespace steps = projt::launch::steps; +using projt::launch::LaunchPipeline; + +LaunchController::LaunchController() = default; + +void LaunchController::executeTask() +{ + if (!m_instance) + { + emitFailed(tr("No instance specified!")); + return; + } + + if (!JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget)) + { + emitFailed(tr("Invalid Java arguments specified. Please fix this first.")); + return; + } + + login(); +} + +void LaunchController::decideAccount() +{ + if (m_accountToUse) + { + return; + } + + auto accounts = APPLICATION->accounts(); + auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); + auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); + if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) + { + m_accountToUse = accounts->defaultAccount(); + } + else + { + m_accountToUse = accounts->at(instanceAccountIndex); + } + + if (!accounts->anyAccountIsValid()) + { + auto reply = CustomMessageBox::selectable(m_parentWidget, + tr("No Accounts"), + tr("In order to play Minecraft, you must have at least one Microsoft " + "account which owns Minecraft logged in. " + "Would you like to open the account manager to add an account now?"), + QMessageBox::Information, + QMessageBox::Yes | QMessageBox::No) + ->exec(); + + if (reply == QMessageBox::Yes) + { + APPLICATION->ShowGlobalSettings(m_parentWidget, "accounts"); + } + else if (reply == QMessageBox::No) + { + return; + } + } + + if (!m_accountToUse) + { + ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), + ProfileSelectDialog::GlobalDefaultCheckbox, + m_parentWidget); + + selectDialog.exec(); + m_accountToUse = selectDialog.selectedAccount(); + + if (selectDialog.useAsGlobalDefault() && m_accountToUse) + { + accounts->setDefaultAccount(m_accountToUse); + } + } +} + +LaunchDecision LaunchController::decideLaunchMode() +{ + if (!m_accountToUse || m_wantedLaunchMode == LaunchMode::Demo) + { + m_actualLaunchMode = LaunchMode::Demo; + return LaunchDecision::Continue; + } + + if (m_wantedLaunchMode == LaunchMode::Normal + && (m_accountToUse->shouldRefresh() || m_accountToUse->accountState() == AccountState::Offline)) + { + m_accountToUse->refresh(); + } + + const auto accounts = APPLICATION->accounts(); + MinecraftAccountPtr accountToCheck = nullptr; + + if (m_accountToUse->accountType() != AccountType::Offline) + { + accountToCheck = m_accountToUse->ownsMinecraft() ? m_accountToUse : nullptr; + } + else if (const auto defaultAccount = accounts->defaultAccount(); + defaultAccount && defaultAccount->ownsMinecraft()) + { + accountToCheck = defaultAccount; + } + else + { + for (int i = 0; i < accounts->count(); i++) + { + if (const auto account = accounts->at(i); account->ownsMinecraft()) + { + accountToCheck = account; + break; + } + } + } + + if (!accountToCheck) + { + m_actualLaunchMode = LaunchMode::Demo; + return LaunchDecision::Continue; + } + + auto state = accountToCheck->accountState(); + if (state == AccountState::Unchecked || state == AccountState::Errored) + { + accountToCheck->refresh(); + state = AccountState::Working; + } + + if (state == AccountState::Working) + { + ProgressDialog progDialog(m_parentWidget); + progDialog.setSkipButton(true, tr("Abort")); + + auto task = accountToCheck->currentTask(); + progDialog.execWithTask(*task); + + if (task->getState() == Task::State::AbortedByUser) + { + return LaunchDecision::Abort; + } + + state = accountToCheck->accountState(); + } + + QString reauthReason; + switch (state) + { + case AccountState::Errored: + case AccountState::Expired: + reauthReason = tr("'%1' has expired and needs to be reauthenticated").arg(accountToCheck->profileName()); + break; + case AccountState::Disabled: + reauthReason = tr("The launcher's client identification has changed"); + break; + case AccountState::Gone: + reauthReason = tr("'%1' no longer exists on the servers").arg(accountToCheck->profileName()); + break; + default: + m_actualLaunchMode = + state == AccountState::Online && m_wantedLaunchMode == LaunchMode::Normal ? LaunchMode::Normal + : LaunchMode::Offline; + return LaunchDecision::Continue; + } + + if (reauthenticateAccount(accountToCheck, reauthReason)) + { + return LaunchDecision::Undecided; + } + + return LaunchDecision::Abort; +} + +bool LaunchController::askPlayDemo() +{ + QMessageBox box(m_parentWidget); + box.setWindowTitle(tr("Play demo?")); + + QString text = m_accountToUse + ? tr("This account does not own Minecraft.\nYou need to purchase the game first to play the full version.") + : tr("No account was selected for launch."); + text += tr("\n\nDo you want to play the demo?"); + + box.setText(text); + box.setIcon(QMessageBox::Warning); + auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); + auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + box.setDefaultButton(cancelButton); + + box.exec(); + return box.clickedButton() == demoButton; +} + +QString LaunchController::askOfflineName(QString playerName, bool* ok) +{ + if (ok) + { + *ok = false; + } + + QString message; + switch (m_actualLaunchMode) + { + case LaunchMode::Normal: + Q_ASSERT(false); + return {}; + case LaunchMode::Demo: + message = tr("Choose your demo mode player name."); + break; + case LaunchMode::Offline: + if (m_wantedLaunchMode == LaunchMode::Normal) + { + message = tr("You are not connected to the Internet, launching in offline mode.\n\n"); + } + message += tr("Choose your offline mode player name."); + break; + } + + QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + QString usedName = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; + bool accepted = false; + QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedName, &accepted); + if (!accepted) + { + return {}; + } + if (!name.isEmpty()) + { + usedName = name; + APPLICATION->settings()->set("LastOfflinePlayerName", usedName); + } + if (ok) + { + *ok = true; + } + return usedName; +} + +void LaunchController::login() +{ + decideAccount(); + + LaunchDecision decision = decideLaunchMode(); + while (decision == LaunchDecision::Undecided) + { + decision = decideLaunchMode(); + } + if (decision == LaunchDecision::Abort) + { + emitAborted(); + return; + } + + if (m_actualLaunchMode == LaunchMode::Demo) + { + if (m_wantedLaunchMode == LaunchMode::Demo || askPlayDemo()) + { + bool ok = false; + auto name = askOfflineName("Player", &ok); + if (ok) + { + m_session = std::make_shared(); + static const QRegularExpression s_removeChars("[{}-]"); + m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars)); + launchInstance(); + return; + } + } + + emitFailed(tr("No account selected for launch.")); + return; + } + + m_session = std::make_shared(); + m_session->launchMode = m_actualLaunchMode; + m_accountToUse->fillSession(m_session); + + if (m_accountToUse->accountType() != AccountType::Offline) + { + if (m_actualLaunchMode == LaunchMode::Normal && !m_accountToUse->hasProfile()) + { + ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); + if (dialog.exec() != QDialog::Accepted) + { + emitAborted(); + return; + } + } + + if (m_actualLaunchMode == LaunchMode::Offline) + { + bool ok = false; + QString name = m_offlineName; + if (name.isEmpty()) + { + name = askOfflineName(m_session->player_name, &ok); + if (!ok) + { + emitAborted(); + return; + } + } + m_session->MakeOffline(name); + } + } + + launchInstance(); +} + +bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account, QString reason) +{ + if (reason.isEmpty()) + { + reason = tr("'%1' has expired and needs to be reauthenticated").arg(account->profileName()); + } + + auto button = QMessageBox::warning( + m_parentWidget, + tr("Account refresh failed"), + tr("%1. Do you want to reauthenticate this account?").arg(reason), + QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, + QMessageBox::StandardButton::Yes); + if (button == QMessageBox::StandardButton::Yes) + { + auto accounts = APPLICATION->accounts(); + bool isDefault = accounts->defaultAccount() == account; + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); + if (account->accountType() == AccountType::MSA) + { + auto newAccount = MSALoginDialog::newAccount(m_parentWidget); + + if (newAccount != nullptr) + { + accounts->addAccount(newAccount); + + if (isDefault) + { + accounts->setDefaultAccount(newAccount); + } + + if (m_accountToUse == account) + { + m_accountToUse = nullptr; + decideAccount(); + } + return true; + } + } + } + + emitFailed(reason); + return false; +} + +void LaunchController::launchInstance() +{ + Q_ASSERT_X(m_instance != nullptr, "launchInstance", "instance is NULL"); + Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); + + if (!m_instance->reloadSettings()) + { + QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); + emitFailed(tr("Couldn't load the instance profile.")); + return; + } + + m_launcher = m_instance->createLaunchPipeline(m_session, m_targetToJoin); + if (!m_launcher) + { + emitFailed(tr("Couldn't instantiate a launcher.")); + return; + } + + auto console = qobject_cast(m_parentWidget); + auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); + if (!console && showConsole) + { + APPLICATION->showInstanceWindow(m_instance); + } + connect(m_launcher.get(), &LaunchPipeline::readyForLaunch, this, &LaunchController::readyForLaunch); + connect(m_launcher.get(), &LaunchPipeline::succeeded, this, &LaunchController::onSucceeded); + connect(m_launcher.get(), &LaunchPipeline::failed, this, &LaunchController::onFailed); + connect(m_launcher.get(), &LaunchPipeline::requestProgress, this, &LaunchController::onProgressRequested); + + QString onlineMode; + if (m_actualLaunchMode == LaunchMode::Normal) + { + onlineMode = "online"; + + QStringList servers = { "login.microsoftonline.com", + "session.minecraft.net", + "textures.minecraft.net", + "api.mojang.com" }; + m_launcher->prependStage(makeShared(m_launcher.get(), servers)); + } + else + { + onlineMode = m_actualLaunchMode == LaunchMode::Demo ? "demo" : "offline"; + } + + m_launcher->prependStage(makeShared(m_launcher.get(), + "Launched instance in " + onlineMode + " mode\n", + MessageLevel::Launcher)); + + auto versionString = QString("%1 version: %2 (%3)") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME, + BuildConfig.printableVersionString(), + BuildConfig.BUILD_PLATFORM); + m_launcher->prependStage( + makeShared(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher)); + m_launcher->start(); +} + +void LaunchController::readyForLaunch() +{ + if (!m_profiler) + { + m_launcher->proceed(); + return; + } + + QString error; + if (!m_profiler->check(&error)) + { + m_launcher->abort(); + emitFailed("Profiler startup failed!"); + QMessageBox::critical(m_parentWidget, + tr("Error!"), + tr("Profiler check for %1 failed: %2").arg(m_profiler->name(), error)); + return; + } + BaseProfiler* profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); + + connect(profilerInstance, + &BaseProfiler::readyToLaunch, + [this](const QString& message) + { + QMessageBox msg(m_parentWidget); + msg.setText(tr("The game launch is delayed until you press the " + "button. This is the right time to setup the profiler, as the " + "profiler server is running now.\n\n%1") + .arg(message)); + msg.setWindowTitle(tr("Waiting.")); + msg.setIcon(QMessageBox::Information); + msg.addButton(tr("&Launch"), QMessageBox::AcceptRole); + msg.exec(); + m_launcher->proceed(); + }); + connect(profilerInstance, + &BaseProfiler::abortLaunch, + [this](const QString& message) + { + QMessageBox msg; + msg.setText(tr("Couldn't start the profiler: %1").arg(message)); + msg.setWindowTitle(tr("Error")); + msg.setIcon(QMessageBox::Critical); + msg.addButton(QMessageBox::Ok); + msg.setModal(true); + msg.exec(); + m_launcher->abort(); + emitFailed("Profiler startup failed!"); + }); + profilerInstance->beginProfiling(m_launcher); +} + +void LaunchController::onSucceeded() +{ + emitSucceeded(); +} + +void LaunchController::onFailed(QString reason) +{ + if (m_instance->settings()->get("ShowConsoleOnError").toBool()) + { + APPLICATION->showInstanceWindow(m_instance, "console"); + } + emitFailed(reason); +} + +void LaunchController::onProgressRequested(Task* task) +{ + if (!task) + { + return; + } + + ProgressDialog progDialog(m_parentWidget); + progDialog.setSkipButton(true, tr("Abort")); + m_launcher->proceed(); + progDialog.execWithTask(*task); +} + +bool LaunchController::abort() +{ + if (!m_launcher) + { + return true; + } + if (!m_launcher->canAbort()) + { + return false; + } + auto response = CustomMessageBox::selectable( + m_parentWidget, + tr("Kill Minecraft?"), + tr("This can cause the instance to get corrupted and should only be used if Minecraft " + "is frozen for some reason"), + QMessageBox::Question, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes) + ->exec(); + if (response == QMessageBox::Yes) + { + return m_launcher->abort(); + } + return false; +} diff --git a/archived/projt-launcher/launcher/LaunchController.h b/archived/projt-launcher/launcher/LaunchController.h new file mode 100644 index 0000000000..386bed9f02 --- /dev/null +++ b/archived/projt-launcher/launcher/LaunchController.h @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +#include "LaunchMode.h" +#include "minecraft/auth/MinecraftAccount.hpp" +#include "minecraft/launch/MinecraftTarget.hpp" + +class InstanceWindow; + +namespace projt::launch +{ + class LaunchPipeline; +} + +enum class LaunchDecision +{ + Undecided, + Continue, + Abort +}; + +class LaunchController : public Task +{ + Q_OBJECT + + public: + void executeTask() override; + + LaunchController(); + ~LaunchController() override = default; + + void setInstance(InstancePtr instance) + { + m_instance = instance; + } + + InstancePtr instance() + { + return m_instance; + } + + void setLaunchMode(LaunchMode mode) + { + m_wantedLaunchMode = mode; + } + + void setOfflineName(const QString& offlineName) + { + m_offlineName = offlineName; + } + + void setProfiler(BaseProfilerFactory* profiler) + { + m_profiler = profiler; + } + + void setParentWidget(QWidget* widget) + { + m_parentWidget = widget; + } + + void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) + { + m_targetToJoin = std::move(targetToJoin); + } + + void setAccountToUse(MinecraftAccountPtr accountToUse) + { + m_accountToUse = std::move(accountToUse); + } + + QString id() + { + return m_instance->id(); + } + + bool abort() override; + + private: + void login(); + void launchInstance(); + void decideAccount(); + LaunchDecision decideLaunchMode(); + bool askPlayDemo(); + QString askOfflineName(QString playerName, bool* ok = nullptr); + bool reauthenticateAccount(MinecraftAccountPtr account, QString reason = {}); + + private slots: + void readyForLaunch(); + void onSucceeded(); + void onFailed(QString reason); + void onProgressRequested(Task* task); + + private: + LaunchMode m_wantedLaunchMode = LaunchMode::Normal; + LaunchMode m_actualLaunchMode = LaunchMode::Normal; + BaseProfilerFactory* m_profiler = nullptr; + QString m_offlineName; + InstancePtr m_instance; + QWidget* m_parentWidget = nullptr; + InstanceWindow* m_console = nullptr; + MinecraftAccountPtr m_accountToUse = nullptr; + AuthSessionPtr m_session; + shared_qobject_ptr m_launcher; + MinecraftTarget::Ptr m_targetToJoin; +}; diff --git a/archived/projt-launcher/launcher/LaunchMode.h b/archived/projt-launcher/launcher/LaunchMode.h new file mode 100644 index 0000000000..efef8b0255 --- /dev/null +++ b/archived/projt-launcher/launcher/LaunchMode.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +enum class LaunchMode +{ + Normal, + Offline, + Demo, +}; diff --git a/archived/projt-launcher/launcher/Launcher.in b/archived/projt-launcher/launcher/Launcher.in new file mode 100755 index 0000000000..6428bf42b0 --- /dev/null +++ b/archived/projt-launcher/launcher/Launcher.in @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Basic start script for running the launcher with the libs packaged with it. + +function printerror { + printf "$1" + if which zenity >/dev/null; then zenity --error --text="$1" &>/dev/null; + elif which kdialog >/dev/null; then kdialog --error "$1" &>/dev/null; + fi +} + +if [[ $EUID -eq 0 ]]; then + printerror "This program should not be run using sudo or as the root user!\n" + exit 1 +fi + + +LAUNCHER_NAME=@Launcher_AppBinaryName@ +LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" +echo "Launcher Dir: ${LAUNCHER_DIR}" + +# Set up env. +# Pass our custom variables separately so that the launcher can remove them for child processes +export LAUNCHER_LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@" +export LAUNCHER_LD_PRELOAD="" +export LAUNCHER_QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins" +export LAUNCHER_QT_FONTPATH="${LAUNCHER_DIR}/fonts" + +export LD_LIBRARY_PATH="$LAUNCHER_LD_LIBRARY_PATH:$LD_LIBRARY_PATH" +export LD_PRELOAD="$LAUNCHER_LD_PRELOAD:$LD_PRELOAD" +export QT_PLUGIN_PATH="$LAUNCHER_QT_PLUGIN_PATH:$QT_PLUGIN_PATH" +export QT_FONTPATH="$LAUNCHER_QT_FONTPATH:$QT_FONTPATH" + +# Detect missing dependencies... +DEPS_LIST=`ldd "${LAUNCHER_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'` +if [ "x$DEPS_LIST" = "x" ]; then + # We have all our dependencies. Run the launcher. + echo "No missing dependencies found." + + # Just to be sure... + chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" + + ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") + + if [ -f portable.txt ]; then + ARGS+=("-d" "${LAUNCHER_DIR}") + fi + + ARGS+=("$@") + + # Run the launcher + exec -a "${ARGS[@]}" + + # Run the launcher in valgrind + # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" + + # Run the launcher with callgrind, delay instrumentation + # valgrind --log-file="valgrind.log" --tool=callgrind --instr-atstart=no "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" + # use callgrind_control -i on/off to profile actions + + # Exit with launcher's exit code. + # exit $? +else + # apt + if which apt-file &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do apt-file -l search $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo apt-get install $COMMAND_LIBS" + # pacman + elif which pkgfile &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pkgfile $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo pacman -S $COMMAND_LIBS" + # dnf + elif which dnf &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do dnf whatprovides -q $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | grep -v 'Repo' | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo dnf install $COMMAND_LIBS" + # yum + elif which yum &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do yum whatprovides $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo yum install $COMMAND_LIBS" + # zypper + elif which zypper &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do zypper wp $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo zypper install $COMMAND_LIBS" + # emerge + elif which pfl &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pfl $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo emerge $COMMAND_LIBS" + fi + + MESSAGE="Error: The launcher is missing the following libraries that it needs to work correctly:\n\t${DEPS_LIST}\nPlease install them from your distribution's package manager." + MESSAGE="$MESSAGE\n\nHint (please apply common sense): $INSTALL_CMD\n" + + printerror "$MESSAGE" + exit 1 +fi diff --git a/archived/projt-launcher/launcher/LoggedProcess.cpp b/archived/projt-launcher/launcher/LoggedProcess.cpp new file mode 100644 index 0000000000..5ac41d6dd6 --- /dev/null +++ b/archived/projt-launcher/launcher/LoggedProcess.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022,2023 Sefa Eyeoglu + * Copyright (c) 2023 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include "LoggedProcess.h" +#include +#include "MessageLevel.h" + +LoggedProcess::LoggedProcess(QStringConverter::Encoding encoding, QObject* parent) + : QProcess(parent), + m_err_decoder(encoding), + m_out_decoder(encoding) +{ + // QProcess has a strange interface... let's map a lot of those into a few. + connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); + connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); + connect(this, &QProcess::finished, this, &LoggedProcess::on_exit); + connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error); + connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); +} + +LoggedProcess::~LoggedProcess() +{ + if (m_is_detachable) + { + setProcessState(QProcess::NotRunning); + } +} + +QStringList LoggedProcess::reprocess(const QByteArray& data, QStringDecoder& decoder) +{ + QString str = decoder(data); + + if (!m_leftover_line.isEmpty()) + { + str.prepend(m_leftover_line); + m_leftover_line = ""; + } + + auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed); + + m_leftover_line = lines.takeLast(); + return lines; +} + +void LoggedProcess::on_stdErr() +{ + auto lines = reprocess(readAllStandardError(), m_err_decoder); + emit log(lines, MessageLevel::StdErr); +} + +void LoggedProcess::on_stdOut() +{ + auto lines = reprocess(readAllStandardOutput(), m_out_decoder); + emit log(lines, MessageLevel::StdOut); +} + +void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status) +{ + // save the exit code + m_exit_code = exit_code; + + // based on state, send signals + if (!m_is_aborting) + { + if (status == QProcess::NormalExit) + { + //: Message displayed on instance exit + emit log({ tr("Process exited with code %1.").arg(exit_code) }, MessageLevel::Launcher); + changeState(LoggedProcess::Finished); + } + else + { + //: Message displayed on instance crashed + if (exit_code == -1) + emit log({ tr("Process crashed.") }, MessageLevel::Launcher); + else + emit log({ tr("Process crashed with exitcode %1.").arg(exit_code) }, MessageLevel::Launcher); + changeState(LoggedProcess::Crashed); + } + } + else + { + //: Message displayed after the instance exits due to kill request + emit log({ tr("Process was killed by user.") }, MessageLevel::Error); + changeState(LoggedProcess::Aborted); + } +} + +void LoggedProcess::on_error(QProcess::ProcessError error) +{ + switch (error) + { + case QProcess::FailedToStart: + { + emit log({ tr("The process failed to start.") }, MessageLevel::Fatal); + changeState(LoggedProcess::FailedToStart); + break; + } + // we'll just ignore those... never needed them + case QProcess::Crashed: + case QProcess::ReadError: + case QProcess::Timedout: + case QProcess::UnknownError: + case QProcess::WriteError: break; + } +} + +void LoggedProcess::kill() +{ + m_is_aborting = true; + QProcess::kill(); +} + +int LoggedProcess::exitCode() const +{ + return m_exit_code; +} + +void LoggedProcess::changeState(LoggedProcess::State state) +{ + if (state == m_state) + return; + m_state = state; + emit stateChanged(m_state); +} + +LoggedProcess::State LoggedProcess::state() const +{ + return m_state; +} + +void LoggedProcess::on_stateChange(QProcess::ProcessState state) +{ + switch (state) + { + case QProcess::NotRunning: break; // let's not - there are too many that handle this already. + case QProcess::Starting: + { + if (m_state != LoggedProcess::NotRunning) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" + << (int)LoggedProcess::Starting; + } + changeState(LoggedProcess::Starting); + return; + } + case QProcess::Running: + { + if (m_state != LoggedProcess::Starting) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" + << (int)LoggedProcess::Running; + } + changeState(LoggedProcess::Running); + return; + } + } +} + +void LoggedProcess::setDetachable(bool detachable) +{ + m_is_detachable = detachable; +} diff --git a/archived/projt-launcher/launcher/LoggedProcess.h b/archived/projt-launcher/launcher/LoggedProcess.h new file mode 100644 index 0000000000..4d4917d808 --- /dev/null +++ b/archived/projt-launcher/launcher/LoggedProcess.h @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022,2023 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#pragma once + +#include +#include +#include +#include "MessageLevel.h" + +/* + * This is a basic process. + * It has line-based logging support and hides some of the nasty bits. + */ +class LoggedProcess : public QProcess +{ + Q_OBJECT + public: + enum State + { + NotRunning, + Starting, + FailedToStart, + Running, + Finished, + Crashed, + Aborted + }; + + public: + explicit LoggedProcess(QStringConverter::Encoding encoding = QStringConverter::System, QObject* parent = nullptr); + virtual ~LoggedProcess(); + + State state() const; + int exitCode() const; + + void setDetachable(bool detachable); + + signals: + void log(QStringList lines, MessageLevel::Enum level); + void stateChanged(LoggedProcess::State state); + + public slots: + /** + * @brief kill the process - equivalent to kill -9 + */ + void kill(); + + private slots: + void on_stdErr(); + void on_stdOut(); + void on_exit(int exit_code, QProcess::ExitStatus status); + void on_error(QProcess::ProcessError error); + void on_stateChange(QProcess::ProcessState); + + private: + void changeState(LoggedProcess::State state); + + QStringList reprocess(const QByteArray& data, QStringDecoder& decoder); + + private: + QStringDecoder m_err_decoder; + QStringDecoder m_out_decoder; + QString m_leftover_line; + bool m_killed = false; + State m_state = NotRunning; + int m_exit_code = 0; + bool m_is_aborting = false; + bool m_is_detachable = false; +}; diff --git a/archived/projt-launcher/launcher/MMCTime.cpp b/archived/projt-launcher/launcher/MMCTime.cpp new file mode 100644 index 0000000000..41f5b51f0a --- /dev/null +++ b/archived/projt-launcher/launcher/MMCTime.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2015 Petr Mrazek + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include +#include + +#include +#include +#include + +QString Time::prettifyDuration(int64_t duration, bool noDays) +{ + int seconds = (int)(duration % 60); + duration /= 60; + int minutes = (int)(duration % 60); + duration /= 60; + int hours = (int)(noDays ? duration : (duration % 24)); + int days = (int)(noDays ? 0 : (duration / 24)); + if ((hours == 0) && (days == 0)) + { + return QObject::tr("%1min %2s").arg(minutes).arg(seconds); + } + if (days == 0) + { + return QObject::tr("%1h %2min").arg(hours).arg(minutes); + } + return QObject::tr("%1d %2h %3min").arg(days).arg(hours).arg(minutes); +} + +QString Time::humanReadableDuration(double duration, int precision) +{ + using days = std::chrono::duration>; + + QString outStr; + QTextStream os(&outStr); + + bool neg = false; + if (duration < 0) + { + neg = true; // flag + duration *= -1; // invert + } + + auto std_duration = std::chrono::duration(duration); + auto d = std::chrono::duration_cast(std_duration); + std_duration -= d; + auto h = std::chrono::duration_cast(std_duration); + std_duration -= h; + auto m = std::chrono::duration_cast(std_duration); + std_duration -= m; + auto s = std::chrono::duration_cast(std_duration); + std_duration -= s; + auto ms = std::chrono::duration_cast(std_duration); + + auto dc = d.count(); + auto hc = h.count(); + auto mc = m.count(); + auto sc = s.count(); + auto msc = ms.count(); + + if (neg) + { + os << '-'; + } + if (dc) + { + os << dc << QObject::tr("days"); + } + if (hc) + { + if (dc) + os << " "; + os << qSetFieldWidth(2) << hc << QObject::tr("h"); // hours + } + if (mc) + { + if (dc || hc) + os << " "; + os << qSetFieldWidth(2) << mc << QObject::tr("m"); // minutes + } + if (dc || hc || mc || sc) + { + if (dc || hc || mc) + os << " "; + os << qSetFieldWidth(2) << sc << QObject::tr("s"); // seconds + } + if ((msc && (precision > 0)) || !(dc || hc || mc || sc)) + { + if (dc || hc || mc || sc) + os << " "; + os << qSetFieldWidth(0) << qSetRealNumberPrecision(precision) << msc << QObject::tr("ms"); // miliseconds + } + + os.flush(); + + return outStr; +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/MMCTime.h b/archived/projt-launcher/launcher/MMCTime.h new file mode 100644 index 0000000000..d9ac1d4037 --- /dev/null +++ b/archived/projt-launcher/launcher/MMCTime.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ======================================================================== */ + +#pragma once + +#include + +namespace Time +{ + + QString prettifyDuration(int64_t duration, bool noDays = false); + + /** + * @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`. + * miliseconds are only included if `precision` is greater than 0. + * + * @param duration a number of seconds as floating point + * @param precision number of decmial points to display on fractons of a second, defualts to 0. + * @return QString + */ + QString humanReadableDuration(double duration, int precision = 0); +} // namespace Time diff --git a/archived/projt-launcher/launcher/MMCZip.cpp b/archived/projt-launcher/launcher/MMCZip.cpp new file mode 100644 index 0000000000..9c5c095092 --- /dev/null +++ b/archived/projt-launcher/launcher/MMCZip.cpp @@ -0,0 +1,872 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "MMCZip.h" +#include +#include +#include +#include +#include "FileSystem.h" + +#include +#include +#include +#include + +#if defined(LAUNCHER_APPLICATION) +#include +#endif + +namespace +{ + bool isWithinExtractionRoot(const QDir& root, const QString& path) + { + const auto cleanRoot = QDir::cleanPath(root.absolutePath()); + const auto cleanPath = QDir::cleanPath(path); + return cleanPath == cleanRoot || cleanPath.startsWith(cleanRoot + '/'); + } + + bool symlinkEscapesExtractionRoot(QuaZip* zip, const QString& outputPath, const QDir& root) + { + QuaZipFileInfo64 info; + if (!zip->getCurrentFileInfo(&info) || !info.isSymbolicLink()) + { + return false; + } + + QuaZipFile linkFile(zip); + if (!linkFile.open(QIODevice::ReadOnly)) + { + return true; + } + + const auto linkTarget = QFile::decodeName(linkFile.readAll()); + linkFile.close(); + + QString resolvedTarget; + if (QDir::isAbsolutePath(linkTarget)) + { + resolvedTarget = QDir::cleanPath(linkTarget); + } + else + { + const auto outputDir = QFileInfo(outputPath).dir(); + resolvedTarget = QDir::cleanPath(outputDir.absoluteFilePath(linkTarget)); + } + + return !isWithinExtractionRoot(root, resolvedTarget); + } +} // namespace + +namespace MMCZip +{ + // ours + bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const Filter& filter) + { + QuaZip modZip(from.filePath()); + modZip.open(QuaZip::mdUnzip); + + QuaZipFile fileInsideMod(&modZip); + QuaZipFile zipOutFile(into); + for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) + { + QString filename = modZip.getCurrentFileName(); + if (filter && !filter(filename)) + { + qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered"; + continue; + } + if (contained.contains(filename)) + { + qDebug() << "Skipping already contained file " << filename << " from " << from.fileName(); + continue; + } + contained.insert(filename); + + if (!fileInsideMod.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open " << filename << " from " << from.fileName(); + return false; + } + + QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); + + if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) + { + qCritical() << "Failed to open " << filename << " in the jar"; + fileInsideMod.close(); + return false; + } + if (!JlCompress::copyData(fileInsideMod, zipOutFile)) + { + zipOutFile.close(); + fileInsideMod.close(); + qCritical() << "Failed to copy data of " << filename << " into the jar"; + return false; + } + zipOutFile.close(); + fileInsideMod.close(); + } + return true; + } + + bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks) + { + QDir directory(dir); + if (!directory.exists()) + return false; + + for (auto e : files) + { + auto filePath = directory.relativeFilePath(e.absoluteFilePath()); + auto srcPath = e.absoluteFilePath(); + if (followSymlinks) + { + if (e.isSymLink()) + { + srcPath = e.symLinkTarget(); + } + else + { + srcPath = e.canonicalFilePath(); + } + } + if (!JlCompress::compressFile(zip, srcPath, filePath)) + return false; + } + + return true; + } + + bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) + { + QuaZip zip(fileCompressed); + zip.setUtf8Enabled(true); + QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); + if (!zip.open(QuaZip::mdCreate)) + { + FS::deletePath(fileCompressed); + return false; + } + + auto result = compressDirFiles(&zip, dir, files, followSymlinks); + + zip.close(); + if (zip.getZipError() != 0) + { + FS::deletePath(fileCompressed); + return false; + } + + return result; + } + +#if defined(LAUNCHER_APPLICATION) + // ours + bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) + { + QuaZip zipOut(targetJarPath); + zipOut.setUtf8Enabled(true); + if (!zipOut.open(QuaZip::mdCreate)) + { + FS::deletePath(targetJarPath); + qCritical() << "Failed to open the minecraft.jar for modding"; + return false; + } + // Files already added to the jar. + // These files will be skipped. + QSet addedFiles; + + // Modify the jar + // This needs to be done in reverse-order to ensure we respect the loading order of components + for (auto i = mods.crbegin(); i != mods.crend(); i++) + { + const auto* mod = *i; + // do not merge disabled mods. + if (!mod->enabled()) + continue; + if (mod->type() == ResourceType::ZIPFILE) + { + if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) + { + zipOut.close(); + FS::deletePath(targetJarPath); + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; + return false; + } + } + else if (mod->type() == ResourceType::SINGLEFILE) + { + auto filename = mod->fileinfo(); + // Check if file already added to avoid duplicates + if (addedFiles.contains(filename.fileName())) + { + qDebug() << "Skipping duplicate file" << filename.fileName(); + continue; + } + if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) + { + zipOut.close(); + FS::deletePath(targetJarPath); + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; + return false; + } + addedFiles.insert(filename.fileName()); + } + else if (mod->type() == ResourceType::FOLDER) + { + // untested, but seems to be unused / not possible to reach + auto filename = mod->fileinfo(); + QString what_to_zip = filename.absoluteFilePath(); + QDir dir(what_to_zip); + dir.cdUp(); + QString parent_dir = dir.absolutePath(); + auto files = QFileInfoList(); + collectFileListRecursively(what_to_zip, nullptr, &files, nullptr); + + // Remove files that have already been added to avoid duplicates + for (auto e : files) + { + if (addedFiles.contains(e.filePath())) + files.removeAll(e); + } + + if (!compressDirFiles(&zipOut, parent_dir, files)) + { + zipOut.close(); + FS::deletePath(targetJarPath); + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; + return false; + } + qDebug() << "Adding folder " << filename.fileName() << " from " << filename.absoluteFilePath(); + } + else + { + // Make sure we do not continue launching when something is missing or undefined... + zipOut.close(); + FS::deletePath(targetJarPath); + qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar."; + return false; + } + } + + if (!mergeZipFiles(&zipOut, + QFileInfo(sourceJarPath), + addedFiles, + [](const QString key) { return !key.contains("META-INF"); })) + { + zipOut.close(); + FS::deletePath(targetJarPath); + qCritical() << "Failed to insert minecraft.jar contents."; + return false; + } + + // Recompress the jar + zipOut.close(); + if (zipOut.getZipError() != 0) + { + FS::deletePath(targetJarPath); + qCritical() << "Failed to finalize minecraft.jar!"; + return false; + } + return true; + } +#endif + + // ours + QString findFolderOfFileInZip(QuaZip* zip, + const QString& what, + const QStringList& ignore_paths, + const QString& root) + { + QuaZipDir rootDir(zip, root); + for (auto&& fileName : rootDir.entryList(QDir::Files)) + { + if (fileName == what) + return root; + + QCoreApplication::processEvents(); + } + + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) + { + if (ignore_paths.contains(fileName)) + continue; + + QString result = findFolderOfFileInZip(zip, what, ignore_paths, root + fileName); + if (!result.isEmpty()) + return result; + } + + return {}; + } + + // ours + bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root) + { + QuaZipDir rootDir(zip, root); + for (auto fileName : rootDir.entryList(QDir::Files)) + { + if (fileName == what) + { + result.append(root); + return true; + } + } + for (auto fileName : rootDir.entryList(QDir::Dirs)) + { + findFilesInZip(zip, what, result, root + fileName); + } + return !result.isEmpty(); + } + + // ours + std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target) + { + auto target_top_dir = QUrl::fromLocalFile(target); + + QStringList extracted; + + qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; + auto numEntries = zip->getEntriesCount(); + if (numEntries < 0) + { + qWarning() << "Failed to enumerate files in archive"; + return std::nullopt; + } + else if (numEntries == 0) + { + qDebug() << "Extracting empty archives seems odd..."; + return extracted; + } + else if (!zip->goToFirstFile()) + { + qWarning() << "Failed to seek to first file in zip"; + return std::nullopt; + } + + do + { + QString file_name = zip->getCurrentFileName(); + file_name = FS::RemoveInvalidPathChars(file_name); + if (!file_name.startsWith(subdir)) + continue; + + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); + auto original_name = relative_file_name; + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) + { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) + { + target_file_path = target + '/'; + } + else + { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) + { + qWarning() << "Extracting" << relative_file_name + << "was cancelled, because it was effectively outside of the target path" << target; + return std::nullopt; + } + + if (!JlCompress::extractFile(zip, "", target_file_path)) + { + qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; + JlCompress::removeFile(extracted); + return std::nullopt; + } + + extracted.append(target_file_path); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) + { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser + | QFileDevice::Permission::ExeUser | QFileDevice::Permission::ReadGroup + | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) + { + if (!QFile::setPermissions(target_file_path, newPermisions)) + { + qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + else if (fileInfo.isDir()) + { + // Ensure the folder has the minimal required permissions + QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner + | QFile::ReadGroup | QFile::ExeGroup | QFile::ReadOther + | QFile::ExeOther; + + QFile::Permissions currentPermissions = fileInfo.permissions(); + if ((currentPermissions & minimalPermissions) != minimalPermissions) + { + if (!QFile::setPermissions(target_file_path, minimalPermissions)) + { + qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + } + while (zip->goToNextFile()); + + return extracted; + } + + // ours + bool extractRelFile(QuaZip* zip, const QString& file, const QString& target) + { + return JlCompress::extractFile(zip, file, target); + } + + // ours + std::optional extractDir(QString fileCompressed, QString dir) + { + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) + { + return QStringList(); + } + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); + ; + return std::nullopt; + } + return extractSubDir(&zip, "", dir); + } + + // ours + std::optional extractDir(QString fileCompressed, QString subdir, QString dir) + { + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) + { + return QStringList(); + } + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); + ; + return std::nullopt; + } + return extractSubDir(&zip, subdir, dir); + } + + // ours + bool extractFile(QString fileCompressed, QString file, QString target) + { + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) + { + return true; + } + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); + return false; + } + return extractRelFile(&zip, file, target); + } + + bool collectFileListRecursively(const QString& rootDir, + const QString& subDir, + QFileInfoList* files, + FilterFileFunction excludeFilter) + { + QDir rootDirectory(rootDir); + if (!rootDirectory.exists()) + return false; + + QDir directory; + if (subDir == nullptr) + directory = rootDirectory; + else + directory = QDir(subDir); + + if (!directory.exists()) + return false; // shouldn't ever happen + + // recurse directories + QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); + for (const auto& e : entries) + { + if (!collectFileListRecursively(rootDir, e.filePath(), files, excludeFilter)) + return false; + } + + // collect files + entries = directory.entryInfoList(QDir::Files); + for (const auto& e : entries) + { + if (excludeFilter && excludeFilter(e)) + { + QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); + qDebug() << "Skipping file " << relativeFilePath; + continue; + } + + files->append(e); // we want the original paths for compressDirFiles + } + return true; + } + +#if defined(LAUNCHER_APPLICATION) + void ExportToZipTask::executeTask() + { + setStatus("Adding files..."); + setProgress(0, m_files.length()); + m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); + connect(&m_build_zip_watcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); + m_build_zip_watcher.setFuture(m_build_zip_future); + } + + auto ExportToZipTask::exportZip() -> ZipResult + { + if (!m_dir.exists()) + { + return ZipResult(tr("Folder doesn't exist")); + } + if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) + { + return ZipResult(tr("Could not create file")); + } + + for (auto fileName : m_extra_files.keys()) + { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + QuaZipFile indexFile(&m_output); + if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) + { + return ZipResult(tr("Could not create:") + fileName); + } + indexFile.write(m_extra_files[fileName]); + } + + for (const QFileInfo& file : m_files) + { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + + auto absolute = file.absoluteFilePath(); + auto relative = m_dir.relativeFilePath(absolute); + setStatus("Compressing: " + relative); + setProgress(m_progress + 1, m_progressTotal); + if (m_follow_symlinks) + { + if (file.isSymLink()) + absolute = file.symLinkTarget(); + else + absolute = file.canonicalFilePath(); + } + + if (!m_exclude_files.contains(relative) + && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) + { + return ZipResult(tr("Could not read and compress %1").arg(relative)); + } + } + + m_output.close(); + if (m_output.getZipError() != 0) + { + return ZipResult(tr("A zip error occurred")); + } + return ZipResult(); + } + + void ExportToZipTask::finish() + { + if (!isRunning()) + { + FS::deletePath(m_output_path); + return; + } + if (m_build_zip_future.isCanceled()) + { + FS::deletePath(m_output_path); + emitAborted(); + } + else if (auto result = m_build_zip_future.result(); result.has_value()) + { + FS::deletePath(m_output_path); + emitFailed(result.value()); + } + else + { + emitSucceeded(); + } + } + + bool ExportToZipTask::abort() + { + if (m_build_zip_future.isRunning()) + { + m_build_zip_future.cancel(); + emitAborted(); + return true; + } + return false; + } + + void ExtractZipTask::executeTask() + { + if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } + m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); + connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); + m_zip_watcher.setFuture(m_zip_future); + } + + auto ExtractZipTask::extractZip() -> ZipResult + { + auto target = m_output_dir.absolutePath(); + auto target_top_dir = QUrl::fromLocalFile(target); + QDir target_dir(target); + + QStringList extracted; + + qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target; + auto numEntries = m_input->getEntriesCount(); + if (numEntries < 0) + { + return ZipResult(tr("Failed to enumerate files in archive")); + } + if (numEntries == 0) + { + logWarning(tr("Extracting empty archives seems odd...")); + return ZipResult(); + } + if (!m_input->goToFirstFile()) + { + return ZipResult(tr("Failed to seek to first file in zip")); + } + + setStatus("Extracting files..."); + setProgress(0, numEntries); + do + { + if (m_zip_future.isCanceled()) + return ZipResult(); + setProgress(m_progress + 1, m_progressTotal); + QString file_name = m_input->getCurrentFileName(); + if (!file_name.startsWith(m_subdirectory)) + continue; + + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); + auto original_name = relative_file_name; + setStatus("Unpacking: " + relative_file_name); + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) + { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) + { + target_file_path = target + '/'; + } + else + { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) + { + return ZipResult( + tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") + .arg(relative_file_name, target)); + } + if (symlinkEscapesExtractionRoot(m_input.get(), target_file_path, target_dir)) + { + return ZipResult( + tr("Extracting %1 was cancelled, because it links outside of the target path %2") + .arg(relative_file_name, target)); + } + + if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) + { + JlCompress::removeFile(extracted); + return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); + } + + extracted.append(target_file_path); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) + { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser + | QFileDevice::Permission::ExeUser | QFileDevice::Permission::ReadGroup + | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) + { + if (!QFile::setPermissions(target_file_path, newPermisions)) + { + logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + else if (fileInfo.isDir()) + { + // Ensure the folder has the minimal required permissions + QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner + | QFile::ReadGroup | QFile::ExeGroup | QFile::ReadOther + | QFile::ExeOther; + + QFile::Permissions currentPermissions = fileInfo.permissions(); + if ((currentPermissions & minimalPermissions) != minimalPermissions) + { + if (!QFile::setPermissions(target_file_path, minimalPermissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + } + while (m_input->goToNextFile()); + + return ZipResult(); + } + + void ExtractZipTask::finish() + { + if (!isRunning()) + { + return; + } + if (m_zip_future.isCanceled()) + { + emitAborted(); + } + else if (auto result = m_zip_future.result(); result.has_value()) + { + emitFailed(result.value()); + } + else + { + emitSucceeded(); + } + } + + bool ExtractZipTask::abort() + { + if (m_zip_future.isRunning()) + { + m_zip_future.cancel(); + emitAborted(); + return true; + } + return false; + } + +#endif +} // namespace MMCZip diff --git a/archived/projt-launcher/launcher/MMCZip.h b/archived/projt-launcher/launcher/MMCZip.h new file mode 100644 index 0000000000..aca4daac43 --- /dev/null +++ b/archived/projt-launcher/launcher/MMCZip.h @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(LAUNCHER_APPLICATION) +#include "minecraft/mod/Mod.hpp" +#endif +#include "Filter.h" +#include "tasks/Task.h" + +namespace MMCZip +{ + using FilterFileFunction = std::function; + + /** + * Merge two zip files, using a filter function + */ + bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const Filter& filter = nullptr); + + /** + * Compress directory, by providing a list of files to compress + * \param zip target archive + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data + * \return true for success or false for failure + */ + bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks = false); + + /** + * Compress directory, by providing a list of files to compress + * \param fileCompressed target archive file + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data + * \return true for success or false for failure + */ + bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); + +#if defined(LAUNCHER_APPLICATION) + /** + * take a source jar, add mods to it, resulting in target jar + */ + bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); +#endif + /** + * Find a single file in archive by file name (not path) + * + * \param ignore_paths paths to skip when recursing the search + * + * \return the path prefix where the file is + */ + QString findFolderOfFileInZip(QuaZip* zip, + const QString& what, + const QStringList& ignore_paths = {}, + const QString& root = QString("")); + + /** + * Find a multiple files of the same name in archive by file name + * If a file is found in a path, no deeper paths are searched + * + * \return true if anything was found + */ + bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root = QString()); + + /** + * Extract a subdirectory from an archive + */ + std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target); + + bool extractRelFile(QuaZip* zip, const QString& file, const QString& target); + + /** + * Extract a whole archive. + * + * \param fileCompressed The name of the archive. + * \param dir The directory to extract to, the current directory if left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ + std::optional extractDir(QString fileCompressed, QString dir); + + /** + * Extract a subdirectory from an archive + * + * \param fileCompressed The name of the archive. + * \param subdir The directory within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ + std::optional extractDir(QString fileCompressed, QString subdir, QString dir); + + /** + * Extract a single file from an archive into a directory + * + * \param fileCompressed The name of the archive. + * \param file The file within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return true for success or false for failure + */ + bool extractFile(QString fileCompressed, QString file, QString dir); + + /** + * Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be + * included. + * \param rootDir directory to start off + * \param subDir subdirectory, should be nullptr for first invocation + * \param files resulting list of QFileInfo + * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) + * \return true for success or false for failure + */ + bool collectFileListRecursively(const QString& rootDir, + const QString& subDir, + QFileInfoList* files, + FilterFileFunction excludeFilter); + +#if defined(LAUNCHER_APPLICATION) + class ExportToZipTask : public Task + { + Q_OBJECT + public: + ExportToZipTask(QString outputPath, + QDir dir, + QFileInfoList files, + QString destinationPrefix = "", + bool followSymlinks = false, + bool utf8Enabled = false) + : m_output_path(outputPath), + m_output(outputPath), + m_dir(dir), + m_files(files), + m_destination_prefix(destinationPrefix), + m_follow_symlinks(followSymlinks) + { + setAbortable(true); + m_output.setUtf8Enabled(utf8Enabled); + }; + ExportToZipTask(QString outputPath, + QString dir, + QFileInfoList files, + QString destinationPrefix = "", + bool followSymlinks = false, + bool utf8Enabled = false) + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; + + virtual ~ExportToZipTask() = default; + + void setExcludeFiles(QStringList excludeFiles) + { + m_exclude_files = excludeFiles; + } + void addExtraFile(QString fileName, QByteArray data) + { + m_extra_files.insert(fileName, data); + } + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult exportZip(); + void finish(); + + private: + QString m_output_path; + QuaZip m_output; + QDir m_dir; + QFileInfoList m_files; + QString m_destination_prefix; + bool m_follow_symlinks; + QStringList m_exclude_files; + QHash m_extra_files; + + QFuture m_build_zip_future; + QFutureWatcher m_build_zip_watcher; + }; + + class ExtractZipTask : public Task + { + Q_OBJECT + public: + ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") + : ExtractZipTask(std::make_shared(input), outputDir, subdirectory) + {} + ExtractZipTask(std::shared_ptr input, QDir outputDir, QString subdirectory = "") + : m_input(input), + m_output_dir(outputDir), + m_subdirectory(subdirectory) + {} + virtual ~ExtractZipTask() = default; + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult extractZip(); + void finish(); + + private: + std::shared_ptr m_input; + QDir m_output_dir; + QString m_subdirectory; + + QFuture m_zip_future; + QFutureWatcher m_zip_watcher; + }; +#endif +} // namespace MMCZip diff --git a/archived/projt-launcher/launcher/MTPixmapCache.h b/archived/projt-launcher/launcher/MTPixmapCache.h new file mode 100644 index 0000000000..87d308ad30 --- /dev/null +++ b/archived/projt-launcher/launcher/MTPixmapCache.h @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +#define GET_TYPE() \ + Qt::ConnectionType type; \ + if (QThread::currentThread() != QCoreApplication::instance()->thread()) \ + type = Qt::BlockingQueuedConnection; \ + else \ + type = Qt::DirectConnection; + +#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ + static RET_TYPE NAME() \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ + return ret; \ + } +#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ + static RET_TYPE NAME(PARAM_1_TYPE p1) \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ + return ret; \ + } +#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ + static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, \ + "_" #NAME, \ + type, \ + Q_RETURN_ARG(RET_TYPE, ret), \ + Q_ARG(PARAM_1_TYPE, p1), \ + Q_ARG(PARAM_2_TYPE, p2)); \ + return ret; \ + } + +/** A wrapper around QPixmapCache with thread affinity with the main thread. + */ +class PixmapCache final : public QObject +{ + Q_OBJECT + + public: + PixmapCache(QObject* parent) : QObject(parent) + {} + ~PixmapCache() override = default; + + static PixmapCache& instance() + { + return *s_instance; + } + static void setInstance(PixmapCache* i) + { + s_instance = i; + } + + public: + DEFINE_FUNC_NO_PARAM(cacheLimit, int) + DEFINE_FUNC_NO_PARAM(clear, bool) + DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) + DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) + DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) + DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) + DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) + + // NOTE: Every function returns something non-void to simplify the macros. + private slots: + int _cacheLimit() + { + return QPixmapCache::cacheLimit(); + } + bool _clear() + { + QPixmapCache::clear(); + return true; + } + bool _find(const QString& key, QPixmap* pixmap) + { + return QPixmapCache::find(key, pixmap); + } + bool _find(const QPixmapCache::Key& key, QPixmap* pixmap) + { + return QPixmapCache::find(key, pixmap); + } + bool _insert(const QString& key, const QPixmap& pixmap) + { + return QPixmapCache::insert(key, pixmap); + } + QPixmapCache::Key _insert(const QPixmap& pixmap) + { + return QPixmapCache::insert(pixmap); + } + bool _remove(const QString& key) + { + QPixmapCache::remove(key); + return true; + } + bool _remove(const QPixmapCache::Key& key) + { + QPixmapCache::remove(key); + return true; + } + bool _replace(const QPixmapCache::Key& key, const QPixmap& pixmap) + { + return QPixmapCache::replace(key, pixmap); + } + bool _setCacheLimit(int n) + { + QPixmapCache::setCacheLimit(n); + return true; + } + + /** + * Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is + * increased + * @return if the cache size was increased + */ + bool _markCacheMissByEviciton() + { + static constexpr uint maxCache = static_cast(std::numeric_limits::max()) / 4; + static constexpr uint step = 10240; + static constexpr int oneSecond = 1000; + + auto now = QTime::currentTime(); + if (!m_last_cache_miss_by_eviciton.isNull()) + { + auto diff = m_last_cache_miss_by_eviciton.msecsTo(now); + if (diff < oneSecond) + { // less than a second ago + ++m_consecutive_fast_evicitons; + } + else + { + m_consecutive_fast_evicitons = 0; + } + } + m_last_cache_miss_by_eviciton = now; + if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) + { + // increase the cache size + uint newSize = _cacheLimit() + step; + if (newSize >= maxCache) + { // increase it until you overflow :D + newSize = maxCache; + qDebug() << m_consecutive_fast_evicitons + << tr("pixmap cache misses by eviction happened too fast, doing nothing as the cache size " + "reached it's limit"); + } + else + { + qDebug() << m_consecutive_fast_evicitons + << tr("pixmap cache misses by eviction happened too fast, increasing cache size to") + << static_cast(newSize); + } + _setCacheLimit(static_cast(newSize)); + m_consecutive_fast_evicitons = 0; + return true; + } + return false; + } + + bool _setFastEvictionThreshold(int threshold) + { + m_consecutive_fast_evicitons_threshold = threshold; + return true; + } + + private: + static PixmapCache* s_instance; + QTime m_last_cache_miss_by_eviciton; + int m_consecutive_fast_evicitons = 0; + int m_consecutive_fast_evicitons_threshold = 15; +}; diff --git a/archived/projt-launcher/launcher/MangoHud.cpp b/archived/projt-launcher/launcher/MangoHud.cpp new file mode 100644 index 0000000000..add46c9eba --- /dev/null +++ b/archived/projt-launcher/launcher/MangoHud.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * PrismLauncher - Minecraft Launcher + * Copyright (C) 2022 Jan Drögehoff + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ======================================================================== */ + +#include +#include +#include +#include +#include +#include + +#include "FileSystem.h" +#include "Json.h" +#include "MangoHud.h" + +#ifdef __GLIBC__ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#define UNDEF_GNU_SOURCE +#endif +#include +#include +#endif + +namespace MangoHud +{ + + QString getLibraryString() + { + /** + * Guess MangoHud install location by searching for vulkan layers in this order: + * + * $VK_LAYER_PATH + * $XDG_DATA_DIRS (/usr/local/share/:/usr/share/) + * $XDG_DATA_HOME (~/.local/share) + * /etc + * $XDG_CONFIG_DIRS (/etc/xdg) + * $XDG_CONFIG_HOME (~/.config) + * + * @returns Absolute path of libMangoHud.so if found and empty QString otherwise. + */ + QStringList vkLayerList; + { + QString home = QDir::homePath(); + + QString vkLayerPath = qEnvironmentVariable("VK_LAYER_PATH"); + if (!vkLayerPath.isEmpty()) + { + vkLayerList << vkLayerPath; + } + + QStringList xdgDataDirs = + qEnvironmentVariable("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(QLatin1String(":")); + for (QString dir : xdgDataDirs) + { + vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); + } + + QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME"); + if (xdgDataHome.isEmpty()) + { + xdgDataHome = FS::PathCombine(home, ".local", "share"); + } + vkLayerList << FS::PathCombine(xdgDataHome, "vulkan", "implicit_layer.d"); + + vkLayerList << "/etc"; + + QStringList xdgConfigDirs = qEnvironmentVariable("XDG_CONFIG_DIRS", "/etc/xdg").split(QLatin1String(":")); + for (QString dir : xdgConfigDirs) + { + vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); + } + + QString xdgConfigHome = qEnvironmentVariable("XDG_CONFIG_HOME"); + if (xdgConfigHome.isEmpty()) + { + xdgConfigHome = FS::PathCombine(home, ".config"); + } + vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d"); + } + + for (const QString& vkLayer : vkLayerList) + { + // prefer to use architecture specific vulkan layers + QString currentArch = QSysInfo::currentCpuArchitecture(); + + if (currentArch == "arm64") + { + currentArch = "aarch64"; + } + + QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" }; + + QString filePath{}; + for (const QString& manifestName : manifestNames) + { + QString tryPath = FS::PathCombine(vkLayer, manifestName); + if (QFile::exists(tryPath)) + { + filePath = tryPath; + break; + } + } + + if (filePath.isEmpty()) + { + continue; + } + try + { + auto conf = Json::requireDocument(filePath, vkLayer); + auto confObject = Json::requireObject(conf, vkLayer); + auto layer = Json::ensureObject(confObject, "layer"); + QString libraryName = Json::ensureString(layer, "library_path"); + + if (libraryName.isEmpty()) + { + continue; + } + if (QFileInfo(libraryName).isAbsolute()) + { + return libraryName; + } + +#ifdef __GLIBC__ + // Check whether mangohud is usable on a glibc based system + QString libraryPath = findLibrary(libraryName); + if (!libraryPath.isEmpty()) + { + return libraryPath; + } +#else + // Without glibc return recorded shared library as-is. + return libraryName; +#endif + } + catch (const Exception& e) + {} + } + + return {}; + } + + QString findLibrary(QString libName) + { +#ifdef __GLIBC__ + const char* library = libName.toLocal8Bit().constData(); + + void* handle = dlopen(library, RTLD_NOW); + if (!handle) + { + qCritical() << "dlopen() failed:" << dlerror(); + return {}; + } + + char path[PATH_MAX]; + if (dlinfo(handle, RTLD_DI_ORIGIN, path) == -1) + { + qCritical() << "dlinfo() failed:" << dlerror(); + dlclose(handle); + return {}; + } + + auto fullPath = FS::PathCombine(QString(path), libName); + + dlclose(handle); + return fullPath; +#else + qWarning() << "MangoHud::findLibrary is not implemented on this platform"; + return {}; +#endif + } +} // namespace MangoHud + +#ifdef UNDEF_GNU_SOURCE +#undef _GNU_SOURCE +#undef UNDEF_GNU_SOURCE +#endif diff --git a/archived/projt-launcher/launcher/MangoHud.h b/archived/projt-launcher/launcher/MangoHud.h new file mode 100644 index 0000000000..bb6317de74 --- /dev/null +++ b/archived/projt-launcher/launcher/MangoHud.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * PrismLauncher - Minecraft Launcher + * Copyright (C) 2022 Jan Drögehoff + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ======================================================================== */ + +#pragma once + +#include +#include + +namespace MangoHud +{ + + QString getLibraryString(); + + QString findLibrary(QString libName); +} // namespace MangoHud diff --git a/archived/projt-launcher/launcher/Markdown.cpp b/archived/projt-launcher/launcher/Markdown.cpp new file mode 100644 index 0000000000..debca904c4 --- /dev/null +++ b/archived/projt-launcher/launcher/Markdown.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * PrismLauncher - Minecraft Launcher + * Copyright (C) 2023 Joshua Goins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ======================================================================== */ + +#include "Markdown.h" + +QString markdownToHTML(const QString& markdown) +{ + const QByteArray markdownData = markdown.toUtf8(); + char* buffer = + cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); + + QString htmlStr(buffer); + + free(buffer); + + return htmlStr; +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/Markdown.h b/archived/projt-launcher/launcher/Markdown.h new file mode 100644 index 0000000000..2f55560b00 --- /dev/null +++ b/archived/projt-launcher/launcher/Markdown.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * PrismLauncher - Minecraft Launcher + * Copyright (C) 2023 Joshua Goins + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ======================================================================== */ + +#pragma once + +#include +#include + +QString markdownToHTML(const QString& markdown); \ No newline at end of file diff --git a/archived/projt-launcher/launcher/MessageLevel.cpp b/archived/projt-launcher/launcher/MessageLevel.cpp new file mode 100644 index 0000000000..7ac13d82d6 --- /dev/null +++ b/archived/projt-launcher/launcher/MessageLevel.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "MessageLevel.h" + +MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) +{ + QString name = levelName.toUpper(); + if (name == "LAUNCHER") + return MessageLevel::Launcher; + else if (name == "TRACE") + return MessageLevel::Trace; + else if (name == "DEBUG") + return MessageLevel::Debug; + else if (name == "INFO") + return MessageLevel::Info; + else if (name == "MESSAGE") + return MessageLevel::Message; + else if (name == "WARNING" || name == "WARN") + return MessageLevel::Warning; + else if (name == "ERROR" || name == "CRITICAL") + return MessageLevel::Error; + else if (name == "FATAL") + return MessageLevel::Fatal; + // Skip PrePost, it's not exposed to !![]! + // Also skip StdErr and StdOut + else + return MessageLevel::Unknown; +} + +MessageLevel::Enum MessageLevel::getLevel(QtMsgType type) +{ + switch (type) + { + case QtDebugMsg: return MessageLevel::Debug; + case QtInfoMsg: return MessageLevel::Info; + case QtWarningMsg: return MessageLevel::Warning; + case QtCriticalMsg: return MessageLevel::Error; + case QtFatalMsg: return MessageLevel::Fatal; + default: return MessageLevel::Unknown; + } +} + +MessageLevel::Enum MessageLevel::fromLine(QString& line) +{ + // Level prefix + int endmark = line.indexOf("]!"); + if (line.startsWith("!![") && endmark != -1) + { + auto level = MessageLevel::getLevel(line.left(endmark).mid(3)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} + +MessageLevel::Enum MessageLevel::fromLauncherLine(QString& line) +{ + // Level prefix + int startMark = 0; + while (startMark < line.size() + && (line[startMark].isDigit() || line[startMark].isSpace() || line[startMark] == '.')) + ++startMark; + int endmark = line.indexOf(":"); + if (startMark < line.size() && endmark != -1) + { + auto level = MessageLevel::getLevel(line.left(endmark).mid(startMark)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} diff --git a/archived/projt-launcher/launcher/MessageLevel.h b/archived/projt-launcher/launcher/MessageLevel.h new file mode 100644 index 0000000000..881c9281d4 --- /dev/null +++ b/archived/projt-launcher/launcher/MessageLevel.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +/** + * @brief the MessageLevel Enum + * defines what level a log message is + */ +namespace MessageLevel +{ + enum Enum + { + Unknown, /**< No idea what this is or where it came from */ + StdOut, /**< Undetermined stderr messages */ + StdErr, /**< Undetermined stdout messages */ + Launcher, /**< Launcher Messages */ + Trace, /**< Trace Messages */ + Debug, /**< Debug Messages */ + Info, /**< Info Messages */ + Message, /**< Standard Messages */ + Warning, /**< Warnings */ + Error, /**< Errors */ + Fatal, /**< Fatal Errors */ + }; + MessageLevel::Enum getLevel(const QString& levelName); + MessageLevel::Enum getLevel(QtMsgType type); + + /* Get message level from a line. Line is modified if it was successful. */ + MessageLevel::Enum fromLine(QString& line); + + /* Get message level from a line from the launcher log. Line is modified if it was successful. */ + MessageLevel::Enum fromLauncherLine(QString& line); +} // namespace MessageLevel diff --git a/archived/projt-launcher/launcher/NullInstance.h b/archived/projt-launcher/launcher/NullInstance.h new file mode 100644 index 0000000000..0e80f40578 --- /dev/null +++ b/archived/projt-launcher/launcher/NullInstance.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once +#include "BaseInstance.h" +#include "launch/LaunchPipeline.hpp" + +class NullInstance : public BaseInstance +{ + Q_OBJECT + public: + NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) + : BaseInstance(globalSettings, settings, rootDir) + { + setVersionBroken(true); + } + virtual ~NullInstance() = default; + void saveNow() override + {} + void loadSpecificSettings() override + { + setSpecificSettingsLoaded(true); + } + QString getStatusbarDescription() override + { + return tr("Unknown instance type"); + }; + QSet traits() const override + { + return {}; + }; + QString instanceConfigFolder() const override + { + return instanceRoot(); + }; + shared_qobject_ptr createLaunchPipeline(AuthSessionPtr, + MinecraftTarget::Ptr) override + { + return nullptr; + } + QList createUpdateTask() override + { + return {}; + } + QProcessEnvironment createEnvironment() override + { + return QProcessEnvironment(); + } + QProcessEnvironment createLaunchEnvironment() override + { + return QProcessEnvironment(); + } + QMap getVariables() override + { + return QMap(); + } + QStringList getLogFileSearchPaths() override + { + return {}; + } + QString typeName() const override + { + return "Null"; + } + bool canExport() const override + { + return false; + } + bool canEdit() const override + { + return false; + } + bool canLaunch() const override + { + return false; + } + QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override + { + QStringList out; + out << "Null instance - placeholder."; + return out; + } + QString modsRoot() const override + { + return QString(); + } + void updateRuntimeContext() override + { + // NOOP + } +}; diff --git a/archived/projt-launcher/launcher/PSaveFile.h b/archived/projt-launcher/launcher/PSaveFile.h new file mode 100644 index 0000000000..e5e3c0614e --- /dev/null +++ b/archived/projt-launcher/launcher/PSaveFile.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * ======================================================================== */ +#pragma once + +#include +#include +#include "Application.h" + +#if defined(LAUNCHER_APPLICATION) + +/* PSaveFile + * A class that mimics QSaveFile for Windows. + * + * When reading resources, we need to avoid accessing temporary files + * generated by QSaveFile. If we start reading such a file, we may + * inadvertently keep it open while QSaveFile is trying to remove it, + * or we might detect the file just before it is removed, leading to + * race conditions and errors. + * + * Unfortunately, QSaveFile doesn't provide a way to retrieve the + * temporary file name or to set a specific template for the temporary + * file name it uses. By default, QSaveFile appends a `.XXXXXX` suffix + * to the original file name, where the `XXXXXX` part is dynamically + * generated to ensure uniqueness. + * + * This class acts like a lock by adding and removing the target file + * name into/from a global string set, helping to manage access to + * files during critical operations. + * + * Note: Please do not use the `setFileName` function directly, as it + * is not virtual and cannot be overridden. + */ +class PSaveFile : public QSaveFile +{ + public: + PSaveFile(const QString& name) : QSaveFile(name) + { + addPath(name); + } + PSaveFile(const QString& name, QObject* parent) : QSaveFile(name, parent) + { + addPath(name); + } + virtual ~PSaveFile() + { + if (auto app = APPLICATION_DYN) + { + app->removeQSavePath(m_absoluteFilePath); + } + } + + private: + void addPath(const QString& path) + { + m_absoluteFilePath = QFileInfo(path).absoluteFilePath() + "."; // add dot for tmp files only + if (auto app = APPLICATION_DYN) + { + app->addQSavePath(m_absoluteFilePath); + } + } + QString m_absoluteFilePath; +}; +#else +#define PSaveFile QSaveFile +#endif \ No newline at end of file diff --git a/archived/projt-launcher/launcher/PngReader.cpp b/archived/projt-launcher/launcher/PngReader.cpp new file mode 100644 index 0000000000..63ad417543 --- /dev/null +++ b/archived/projt-launcher/launcher/PngReader.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "PngReader.h" + +#include +#include + +#include +#include +#include + +QString PngReader::s_lastError; + +namespace +{ + + struct PngReadContext + { + const unsigned char* data; + size_t size; + size_t offset; + }; + + void pngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length) + { + PngReadContext* ctx = static_cast(png_get_io_ptr(png_ptr)); + if (ctx->offset + length > ctx->size) + { + png_error(png_ptr, "Read past end of data"); + return; + } + std::memcpy(data, ctx->data + ctx->offset, length); + ctx->offset += length; + } + + void pngErrorCallback(png_structp png_ptr, png_const_charp error_msg) + { + PngReader::setLastError(QString::fromUtf8(error_msg)); + longjmp(png_jmpbuf(png_ptr), 1); + } + + void pngWarningCallback(png_structp, png_const_charp warning_msg) + { + qWarning() << "PNG warning:" << warning_msg; + } + +} // namespace + +QImage PngReader::readFromData(const QByteArray& data) +{ + if (data.size() < 8) + { + s_lastError = "Data too small to be a PNG"; + return QImage(); + } + + // Check PNG signature + if (!isPngData(data)) + { + s_lastError = "Invalid PNG signature"; + return QImage(); + } + + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, pngErrorCallback, pngWarningCallback); + if (!png_ptr) + { + s_lastError = "Failed to create PNG read struct"; + return QImage(); + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) + { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + s_lastError = "Failed to create PNG info struct"; + return QImage(); + } + + QImage result; + + if (setjmp(png_jmpbuf(png_ptr))) + { + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return QImage(); + } + + PngReadContext ctx; + ctx.data = reinterpret_cast(data.constData()); + ctx.size = data.size(); + ctx.offset = 0; + + png_set_read_fn(png_ptr, &ctx, pngReadCallback); + + png_read_info(png_ptr, info_ptr); + + png_uint_32 width = png_get_image_width(png_ptr, info_ptr); + png_uint_32 height = png_get_image_height(png_ptr, info_ptr); + png_byte color_type = png_get_color_type(png_ptr, info_ptr); + png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr); + + // Convert to 8-bit RGBA + if (bit_depth == 16) + { + png_set_strip_16(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_PALETTE) + { + png_set_palette_to_rgb(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) + { + png_set_expand_gray_1_2_4_to_8(png_ptr); + } + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) + { + png_set_tRNS_to_alpha(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) + { + png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); + } + + if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + { + png_set_gray_to_rgb(png_ptr); + } + + png_read_update_info(png_ptr, info_ptr); + + // Allocate row pointers + std::vector row_pointers(height); + result = QImage(width, height, QImage::Format_RGBA8888); + + if (result.isNull()) + { + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + s_lastError = "Failed to allocate QImage"; + return QImage(); + } + + for (png_uint_32 y = 0; y < height; y++) + { + row_pointers[y] = result.scanLine(y); + } + + png_read_image(png_ptr, row_pointers.data()); + png_read_end(png_ptr, nullptr); + + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + + s_lastError.clear(); + return result; +} + +QImage PngReader::readFromFile(const QString& filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) + { + s_lastError = QString("Failed to open file: %1").arg(file.errorString()); + return QImage(); + } + + QByteArray data = file.readAll(); + return readFromData(data); +} + +bool PngReader::isPngFile(const QString& filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) + { + return false; + } + + QByteArray header = file.read(8); + return isPngData(header); +} + +bool PngReader::isPngData(const QByteArray& data) +{ + if (data.size() < 8) + { + return false; + } + + static const unsigned char png_signature[8] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + + return std::memcmp(data.constData(), png_signature, 8) == 0; +} + +QString PngReader::lastError() +{ + return s_lastError; +} + +void PngReader::setLastError(const QString& error) +{ + s_lastError = error; +} diff --git a/archived/projt-launcher/launcher/PngReader.h b/archived/projt-launcher/launcher/PngReader.h new file mode 100644 index 0000000000..088ec6c37e --- /dev/null +++ b/archived/projt-launcher/launcher/PngReader.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +/** + * @brief PNG reader using the bundled libpng and zlib + * + * This class provides PNG reading functionality using the project's own + * libpng and zlib implementations, avoiding conflicts with system libraries. + * + * Qt's QImage uses the system libpng which may be compiled with a different + * zlib version, causing "unsupported zlib version" errors. This class + * bypasses that by using our bundled libraries directly. + */ +class PngReader +{ + public: + /** + * @brief Read a PNG image from a file + * @param filePath Path to the PNG file + * @return QImage containing the decoded image, or null QImage on failure + */ + static QImage readFromFile(const QString& filePath); + + /** + * @brief Read a PNG image from memory + * @param data PNG data in memory + * @return QImage containing the decoded image, or null QImage on failure + */ + static QImage readFromData(const QByteArray& data); + + /** + * @brief Check if a file is a valid PNG + * @param filePath Path to the file + * @return true if the file has a valid PNG signature + */ + static bool isPngFile(const QString& filePath); + + /** + * @brief Check if data is valid PNG + * @param data Data to check + * @return true if the data has a valid PNG signature + */ + static bool isPngData(const QByteArray& data); + + /** + * @brief Get the last error message + * @return Error message from the last failed operation + */ + static QString lastError(); + + /** + * @brief Set the last error message (for internal use by callbacks) + * @param error Error message + */ + static void setLastError(const QString& error); + + private: + static QString s_lastError; +}; diff --git a/archived/projt-launcher/launcher/ProblemProvider.h b/archived/projt-launcher/launcher/ProblemProvider.h new file mode 100644 index 0000000000..4ea16a0198 --- /dev/null +++ b/archived/projt-launcher/launcher/ProblemProvider.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +enum class ProblemSeverity +{ + None, + Warning, + Error +}; + +struct PatchProblem +{ + ProblemSeverity m_severity; + QString m_description; +}; + +class ProblemProvider +{ + public: + virtual ~ProblemProvider() + {} + virtual const QList getProblems() const = 0; + virtual ProblemSeverity getProblemSeverity() const = 0; +}; + +class ProblemContainer : public ProblemProvider +{ + public: + const QList getProblems() const override + { + return m_problems; + } + ProblemSeverity getProblemSeverity() const override + { + return m_problemSeverity; + } + virtual void addProblem(ProblemSeverity severity, const QString& description) + { + if (severity > m_problemSeverity) + { + m_problemSeverity = severity; + } + m_problems.append({ severity, description }); + } + + private: + QList m_problems; + ProblemSeverity m_problemSeverity = ProblemSeverity::None; +}; diff --git a/archived/projt-launcher/launcher/QObjectPtr.h b/archived/projt-launcher/launcher/QObjectPtr.h new file mode 100644 index 0000000000..d32bafb1f3 --- /dev/null +++ b/archived/projt-launcher/launcher/QObjectPtr.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +#include +#include + +/** + * A unique pointer class with unique pointer semantics intended for derivates of QObject + * Calls deleteLater() instead of destroying the contained object immediately + */ +template +using unique_qobject_ptr = QScopedPointer; + +/** + * A shared pointer class with shared pointer semantics intended for derivates of QObject + * Calls deleteLater() instead of destroying the contained object immediately + */ +template +class shared_qobject_ptr : public QSharedPointer +{ + public: + constexpr explicit shared_qobject_ptr() : QSharedPointer() + {} + constexpr explicit shared_qobject_ptr(T* ptr) : QSharedPointer(ptr, &QObject::deleteLater) + {} + constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer(null_ptr, &QObject::deleteLater) + {} + + template + constexpr shared_qobject_ptr(const shared_qobject_ptr& other) : QSharedPointer(other) + {} + + template + constexpr shared_qobject_ptr(const QSharedPointer& other) : QSharedPointer(other) + {} + + void reset() + { + QSharedPointer::reset(); + } + void reset(T* other) + { + shared_qobject_ptr t(other); + this->swap(t); + } + void reset(const shared_qobject_ptr& other) + { + shared_qobject_ptr t(other); + this->swap(t); + } +}; + +template +shared_qobject_ptr makeShared(Args... args) +{ + auto obj = new T(args...); + return shared_qobject_ptr(obj); +} diff --git a/archived/projt-launcher/launcher/QVariantUtils.h b/archived/projt-launcher/launcher/QVariantUtils.h new file mode 100644 index 0000000000..62a9dcab48 --- /dev/null +++ b/archived/projt-launcher/launcher/QVariantUtils.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +namespace QVariantUtils +{ + + template + inline QList toList(QVariant src) + { + QVariantList variantList = src.toList(); + + QList list_t; + list_t.reserve(variantList.size()); + for (const QVariant& v : variantList) + { + list_t.append(v.value()); + } + return list_t; + } + + template + inline QVariant fromList(QList val) + { + QVariantList variantList; + variantList.reserve(val.size()); + for (const T& v : val) + { + variantList.append(v); + } + + return variantList; + } + +} // namespace QVariantUtils \ No newline at end of file diff --git a/archived/projt-launcher/launcher/RWStorage.h b/archived/projt-launcher/launcher/RWStorage.h new file mode 100644 index 0000000000..d58b153e59 --- /dev/null +++ b/archived/projt-launcher/launcher/RWStorage.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include +#include +#include + +template +class RWStorage +{ + public: + void add(K key, V value) + { + QWriteLocker l(&lock); + cache[key] = value; + stale_entries.remove(key); + } + V get(K key) + { + QReadLocker l(&lock); + if (cache.contains(key)) + { + return cache[key]; + } + else + return V(); + } + bool get(K key, V& value) + { + QReadLocker l(&lock); + if (cache.contains(key)) + { + value = cache[key]; + return true; + } + else + return false; + } + bool has(K key) + { + QReadLocker l(&lock); + return cache.contains(key); + } + bool stale(K key) + { + QReadLocker l(&lock); + if (!cache.contains(key)) + return true; + return stale_entries.contains(key); + } + void setStale(K key) + { + QWriteLocker l(&lock); + if (cache.contains(key)) + { + stale_entries.insert(key); + } + } + void clear() + { + QWriteLocker l(&lock); + cache.clear(); + stale_entries.clear(); + } + + private: + QReadWriteLock lock; + QMap cache; + QSet stale_entries; +}; diff --git a/archived/projt-launcher/launcher/RecursiveFileSystemWatcher.cpp b/archived/projt-launcher/launcher/RecursiveFileSystemWatcher.cpp new file mode 100644 index 0000000000..b67c4e7bff --- /dev/null +++ b/archived/projt-launcher/launcher/RecursiveFileSystemWatcher.cpp @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "RecursiveFileSystemWatcher.h" + +#include + +RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) + : QObject(parent), + m_watcher(new QFileSystemWatcher(this)) +{ + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RecursiveFileSystemWatcher::fileChange); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RecursiveFileSystemWatcher::directoryChange); +} + +void RecursiveFileSystemWatcher::setRootDir(const QDir& root) +{ + bool wasEnabled = m_isEnabled; + disable(); + m_root = root; + setFiles(scanRecursive(m_root)); + if (wasEnabled) + { + enable(); + } +} +void RecursiveFileSystemWatcher::setWatchFiles(const bool watchFiles) +{ + bool wasEnabled = m_isEnabled; + disable(); + m_watchFiles = watchFiles; + if (wasEnabled) + { + enable(); + } +} + +void RecursiveFileSystemWatcher::enable() +{ + if (m_isEnabled) + { + return; + } + Q_ASSERT(m_root != QDir::root()); + addFilesToWatcherRecursive(m_root); + m_isEnabled = true; +} +void RecursiveFileSystemWatcher::disable() +{ + if (!m_isEnabled) + { + return; + } + m_isEnabled = false; + m_watcher->removePaths(m_watcher->files()); + m_watcher->removePaths(m_watcher->directories()); +} + +void RecursiveFileSystemWatcher::setFiles(const QStringList& files) +{ + if (files != m_files) + { + m_files = files; + emit filesChanged(); + } +} + +void RecursiveFileSystemWatcher::addFilesToWatcherRecursive(const QDir& dir) +{ + m_watcher->addPath(dir.absolutePath()); + for (const QString& directory : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) + { + addFilesToWatcherRecursive(dir.absoluteFilePath(directory)); + } + if (m_watchFiles) + { + for (const QFileInfo& info : dir.entryInfoList(QDir::Files)) + { + m_watcher->addPath(info.absoluteFilePath()); + } + } +} +QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir& directory) +{ + QStringList ret; + if (!m_matcher) + { + return {}; + } + for (const QString& dir : directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden)) + { + ret.append(scanRecursive(directory.absoluteFilePath(dir))); + } + for (const QString& file : directory.entryList(QDir::Files | QDir::Hidden)) + { + auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); + if (m_matcher(relPath)) + { + ret.append(relPath); + } + } + return ret; +} + +void RecursiveFileSystemWatcher::fileChange(const QString& path) +{ + emit fileChanged(path); +} +void RecursiveFileSystemWatcher::directoryChange([[maybe_unused]] const QString& path) +{ + setFiles(scanRecursive(m_root)); +} diff --git a/archived/projt-launcher/launcher/RecursiveFileSystemWatcher.h b/archived/projt-launcher/launcher/RecursiveFileSystemWatcher.h new file mode 100644 index 0000000000..469f264ec1 --- /dev/null +++ b/archived/projt-launcher/launcher/RecursiveFileSystemWatcher.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include "Filter.h" + +class RecursiveFileSystemWatcher : public QObject +{ + Q_OBJECT + public: + RecursiveFileSystemWatcher(QObject* parent); + + void setRootDir(const QDir& root); + QDir rootDir() const + { + return m_root; + } + + // WARNING: setting this to true may be bad for performance + void setWatchFiles(bool watchFiles); + bool watchFiles() const + { + return m_watchFiles; + } + + void setMatcher(Filter matcher) + { + m_matcher = std::move(matcher); + } + + QStringList files() const + { + return m_files; + } + + signals: + void filesChanged(); + void fileChanged(const QString& path); + + public slots: + void enable(); + void disable(); + + private: + QDir m_root; + bool m_watchFiles = false; + bool m_isEnabled = false; + Filter m_matcher; + + QFileSystemWatcher* m_watcher; + + QStringList m_files; + void setFiles(const QStringList& files); + + void addFilesToWatcherRecursive(const QDir& dir); + QStringList scanRecursive(const QDir& dir); + + private slots: + void fileChange(const QString& path); + void directoryChange(const QString& path); +}; diff --git a/archived/projt-launcher/launcher/ResourceDownloadTask.cpp b/archived/projt-launcher/launcher/ResourceDownloadTask.cpp new file mode 100644 index 0000000000..62b5e90ad8 --- /dev/null +++ b/archived/projt-launcher/launcher/ResourceDownloadTask.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022-2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ResourceDownloadTask.h" + +#include "Application.h" + +#include "minecraft/mod/ModFolderModel.hpp" +#include "minecraft/mod/ResourceFolderModel.hpp" + +#include "modplatform/helpers/HashUtils.h" +#include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" + +ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, + ModPlatform::IndexedVersion version, + const std::shared_ptr packs, + bool is_indexed, + QString custom_target_folder) + : m_pack(std::move(pack)), + m_pack_version(std::move(version)), + m_pack_model(packs), + m_custom_target_folder(custom_target_folder) +{ + if (is_indexed) + { + m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version)); + connect(m_update_task.get(), + &LocalResourceUpdateTask::hasOldResource, + this, + &ResourceDownloadTask::hasOldResource); + + addTask(m_update_task); + } + + m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); + m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); + + QDir dir{ m_pack_model->dir() }; + { + // Use relative path from metadata if available (Generic approach) + if (!m_pack_version.relativePath.isEmpty()) + { + dir.cdUp(); // Go to base (minecraft directory assumption) + dir = QDir(dir.absoluteFilePath(m_pack_version.relativePath)); + } + // Legacy/Manual override + else if (!m_custom_target_folder.isEmpty()) + { + dir.cdUp(); + dir.cd(m_custom_target_folder); + } + } + + auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())); + if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) + { + switch (Hashing::algorithmFromString(m_pack_version.hash_type)) + { + case Hashing::Algorithm::Md4: + action->addValidator( + new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md4, m_pack_version.hash)); + break; + case Hashing::Algorithm::Md5: + action->addValidator( + new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md5, m_pack_version.hash)); + break; + case Hashing::Algorithm::Sha1: + action->addValidator( + new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha1, m_pack_version.hash)); + break; + case Hashing::Algorithm::Sha256: + action->addValidator( + new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_pack_version.hash)); + break; + case Hashing::Algorithm::Sha512: + action->addValidator( + new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha512, m_pack_version.hash)); + break; + default: break; + } + } + m_filesNetJob->addNetAction(action); + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propagateStepProgress); + connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed); + + addTask(m_filesNetJob); +} + +void ResourceDownloadTask::downloadSucceeded() +{ + m_filesNetJob.reset(); + auto name = std::get<0>(to_delete); + auto filename = std::get<1>(to_delete); + if (!name.isEmpty() && filename != m_pack_version.fileName) + m_pack_model->uninstallResource(filename, true); +} + +void ResourceDownloadTask::downloadFailed(QString reason) +{ + emitFailed(reason); + m_filesNetJob.reset(); +} + +void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) +{ + emit progress(current, total); +} + +// This indirection is done so that we don't delete a mod before being sure it was +// downloaded successfully! +void ResourceDownloadTask::hasOldResource(QString name, QString filename) +{ + to_delete = { name, filename }; +} diff --git a/archived/projt-launcher/launcher/ResourceDownloadTask.h b/archived/projt-launcher/launcher/ResourceDownloadTask.h new file mode 100644 index 0000000000..796221fe2b --- /dev/null +++ b/archived/projt-launcher/launcher/ResourceDownloadTask.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022-2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetJob.h" +#include "tasks/SequentialTask.h" + +#include "minecraft/mod/tasks/LocalResourceUpdateTask.hpp" +#include "modplatform/ModIndex.h" + +class ResourceFolderModel; + +class ResourceDownloadTask : public SequentialTask +{ + Q_OBJECT + public: + explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, + ModPlatform::IndexedVersion version, + std::shared_ptr packs, + bool is_indexed = true, + QString custom_target_folder = {}); + const QString& getFilename() const + { + return m_pack_version.fileName; + } + const QString& getCustomPath() const + { + return m_custom_target_folder; + } + const QVariant& getVersionID() const + { + return m_pack_version.fileId; + } + const ModPlatform::IndexedVersion& getVersion() const + { + return m_pack_version; + } + const ModPlatform::ResourceProvider& getProvider() const + { + return m_pack->provider; + } + const QString& getName() const + { + return m_pack->name; + } + ModPlatform::IndexedPack::Ptr getPack() + { + return m_pack; + } + + private: + ModPlatform::IndexedPack::Ptr m_pack; + ModPlatform::IndexedVersion m_pack_version; + const std::shared_ptr m_pack_model; + QString m_custom_target_folder; + + NetJob::Ptr m_filesNetJob; + LocalResourceUpdateTask::Ptr m_update_task; + + void downloadProgressChanged(qint64 current, qint64 total); + void downloadFailed(QString reason); + void downloadSucceeded(); + + std::tuple to_delete{ "", "" }; + + private slots: + void hasOldResource(QString name, QString filename); +}; diff --git a/archived/projt-launcher/launcher/RuntimeContext.h b/archived/projt-launcher/launcher/RuntimeContext.h new file mode 100644 index 0000000000..4a49b74077 --- /dev/null +++ b/archived/projt-launcher/launcher/RuntimeContext.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include "SysInfo.h" +#include "settings/SettingsObject.h" + +struct RuntimeContext +{ + QString javaArchitecture; + QString javaRealArchitecture; + QString system = SysInfo::currentSystem(); + + QString mappedJavaRealArchitecture() const + { + if (javaRealArchitecture == "amd64") + return "x86_64"; + if (javaRealArchitecture == "i386" || javaRealArchitecture == "i686") + return "x86"; + if (javaRealArchitecture == "aarch64") + return "arm64"; + if (javaRealArchitecture == "arm" || javaRealArchitecture == "armhf") + return "arm32"; + return javaRealArchitecture; + } + + void updateFromInstanceSettings(SettingsObjectPtr instanceSettings) + { + javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); + javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); + } + + QString getClassifier() const + { + return system + "-" + mappedJavaRealArchitecture(); + } + + // "Legacy" refers to the fact that Mojang assumed that these are the only two architectures + bool isLegacyArch() const + { + const QString mapped = mappedJavaRealArchitecture(); + return mapped == "x86_64" || mapped == "x86"; + } + + bool classifierMatches(QString target) const + { + // try to match precise classifier "[os]-[arch]" + bool x = target == getClassifier(); + // try to match imprecise classifier on legacy architectures "[os]" + if (!x && isLegacyArch()) + x = target == system; + + return x; + } +}; diff --git a/archived/projt-launcher/launcher/SeparatorPrefixTree.h b/archived/projt-launcher/launcher/SeparatorPrefixTree.h new file mode 100644 index 0000000000..4ab05f235e --- /dev/null +++ b/archived/projt-launcher/launcher/SeparatorPrefixTree.h @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include +#include + +template +class SeparatorPrefixTree +{ + public: + SeparatorPrefixTree(QStringList paths) + { + insert(paths); + } + + SeparatorPrefixTree(bool contained = false) + { + m_contained = contained; + } + + void insert(QStringList paths) + { + for (auto& path : paths) + { + insert(path); + } + } + + /// insert an exact path into the tree + SeparatorPrefixTree& insert(QString path) + { + auto sepIndex = path.indexOf(Tseparator); + if (sepIndex == -1) + { + children[path] = SeparatorPrefixTree(true); + return children[path]; + } + else + { + auto prefix = path.left(sepIndex); + if (!children.contains(prefix)) + { + children[prefix] = SeparatorPrefixTree(false); + } + return children[prefix].insert(path.mid(sepIndex + 1)); + } + } + + /// is the path fully contained in the tree? + bool contains(QString path) const + { + auto node = find(path); + return node != nullptr && node->contained(); + } + + /// does the tree cover a path? That means the prefix of the path is contained in the tree + bool covers(QString path) const + { + // if we found some valid node, it's good enough. the tree covers the path + if (m_contained) + { + return true; + } + auto sepIndex = path.indexOf(Tseparator); + if (sepIndex == -1) + { + auto found = children.find(path); + if (found == children.end()) + { + return false; + } + return (*found).covers(QString()); + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if (found == children.end()) + { + return false; + } + return (*found).covers(path.mid(sepIndex + 1)); + } + } + + /// return the contained path that covers the path specified + QString cover(QString path) const + { + // if we found some valid node, it's good enough. the tree covers the path + if (m_contained) + { + return QString(""); + } + auto sepIndex = path.indexOf(Tseparator); + if (sepIndex == -1) + { + auto found = children.find(path); + if (found == children.end()) + { + return QString(); + } + auto nested = (*found).cover(QString()); + if (nested.isNull()) + { + return nested; + } + if (nested.isEmpty()) + return path; + return path + Tseparator + nested; + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if (found == children.end()) + { + return QString(); + } + auto nested = (*found).cover(path.mid(sepIndex + 1)); + if (nested.isNull()) + { + return nested; + } + if (nested.isEmpty()) + return prefix; + return prefix + Tseparator + nested; + } + } + + /// Does the path-specified node exist in the tree? It does not have to be contained. + bool exists(QString path) const + { + auto sepIndex = path.indexOf(Tseparator); + if (sepIndex == -1) + { + auto found = children.find(path); + if (found == children.end()) + { + return false; + } + return true; + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if (found == children.end()) + { + return false; + } + return (*found).exists(path.mid(sepIndex + 1)); + } + } + + /// find a node in the tree by name + const SeparatorPrefixTree* find(QString path) const + { + auto sepIndex = path.indexOf(Tseparator); + if (sepIndex == -1) + { + auto found = children.find(path); + if (found == children.end()) + { + return nullptr; + } + return &(*found); + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if (found == children.end()) + { + return nullptr; + } + return (*found).find(path.mid(sepIndex + 1)); + } + } + + /// is this a leaf node? + bool leaf() const + { + return children.isEmpty(); + } + + /// is this node actually contained in the tree, or is it purely structural? + bool contained() const + { + return m_contained; + } + + /// Remove a path from the tree + bool remove(QString path) + { + return removeInternal(path) != Failed; + } + + /// Clear all children of this node tree node + void clear() + { + children.clear(); + } + + QStringList toStringList() const + { + QStringList collected; + // collecting these is more expensive. + auto iter = children.begin(); + while (iter != children.end()) + { + QStringList list = iter.value().toStringList(); + for (int i = 0; i < list.size(); i++) + { + list[i] = iter.key() + Tseparator + list[i]; + } + collected.append(list); + if ((*iter).m_contained) + { + collected.append(iter.key()); + } + iter++; + } + return collected; + } + + private: + enum Removal + { + Failed, + Succeeded, + HasChildren + }; + Removal removeInternal(QString path = QString()) + { + if (path.isEmpty()) + { + if (!m_contained) + { + // remove all children - we are removing a prefix + clear(); + return Succeeded; + } + m_contained = false; + if (children.size()) + { + return HasChildren; + } + return Succeeded; + } + Removal remStatus = Failed; + QString childToRemove; + auto sepIndex = path.indexOf(Tseparator); + if (sepIndex == -1) + { + childToRemove = path; + auto found = children.find(childToRemove); + if (found == children.end()) + { + return Failed; + } + remStatus = (*found).removeInternal(); + } + else + { + childToRemove = path.left(sepIndex); + auto found = children.find(childToRemove); + if (found == children.end()) + { + return Failed; + } + remStatus = (*found).removeInternal(path.mid(sepIndex + 1)); + } + switch (remStatus) + { + case Failed: + case HasChildren: + { + return remStatus; + } + case Succeeded: + { + children.remove(childToRemove); + if (m_contained) + { + return HasChildren; + } + if (children.size()) + { + return HasChildren; + } + return Succeeded; + } + } + return Failed; + } + + private: + QMap> children; + bool m_contained = false; +}; diff --git a/archived/projt-launcher/launcher/StringUtils.cpp b/archived/projt-launcher/launcher/StringUtils.cpp new file mode 100644 index 0000000000..efab942aaf --- /dev/null +++ b/archived/projt-launcher/launcher/StringUtils.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "StringUtils.h" +#include + +#include +#include +#include + +/// If you're wondering where these came from exactly, then know you're not the only one =D + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +static inline QChar getNextChar(const QString& s, int location) +{ + return (location < s.length()) ? s.at(location) : QChar(); +} + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) +{ + int l1 = 0, l2 = 0; + while (l1 <= s1.size() && l2 <= s2.size()) + { + // skip spaces, tabs and 0's + QChar c1 = getNextChar(s1, l1); + while (c1.isSpace()) + c1 = getNextChar(s1, ++l1); + + QChar c2 = getNextChar(s2, l2); + while (c2.isSpace()) + c2 = getNextChar(s2, ++l2); + + if (c1.isDigit() && c2.isDigit()) + { + while (c1.digitValue() == 0) + c1 = getNextChar(s1, ++l1); + while (c2.digitValue() == 0) + c2 = getNextChar(s2, ++l2); + + int lookAheadLocation1 = l1; + int lookAheadLocation2 = l2; + int currentReturnValue = 0; + // find the last digit, setting currentReturnValue as we go if it isn't equal + for (QChar lookAhead1 = c1, lookAhead2 = c2; + (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); + lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) + { + bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); + bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); + if (!is1ADigit && !is2ADigit) + break; + if (!is1ADigit) + return -1; + if (!is2ADigit) + return 1; + if (currentReturnValue == 0) + { + if (lookAhead1 < lookAhead2) + { + currentReturnValue = -1; + } + else if (lookAhead1 > lookAhead2) + { + currentReturnValue = 1; + } + } + } + if (currentReturnValue != 0) + return currentReturnValue; + } + + if (cs == Qt::CaseInsensitive) + { + if (!c1.isLower()) + c1 = c1.toLower(); + if (!c2.isLower()) + c2 = c2.toLower(); + } + + int r = QString::localeAwareCompare(c1, c2); + if (r < 0) + return -1; + if (r > 0) + return 1; + + l1 += 1; + l2 += 1; + } + + // The two strings are the same (02 == 2) so fall back to the normal sort + return QString::compare(s1, s2, cs); +} + +QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit) +{ + auto display_options = QUrl::RemoveUserInfo | QUrl::RemoveFragment | QUrl::NormalizePathSegments; + auto str_url = url.toDisplayString(display_options); + + if (str_url.length() <= max_len) + return str_url; + + auto url_path_parts = url.path().split('/'); + QString last_path_segment = url_path_parts.takeLast(); + + if (url_path_parts.size() >= 1 && url_path_parts.first().isEmpty()) + url_path_parts.removeFirst(); // drop empty first segment (from leading / ) + + if (url_path_parts.size() >= 1) + url_path_parts.removeLast(); // drop the next to last path segment + + auto url_template = QStringLiteral("%1://%2/%3%4"); + + auto url_compact = + url_path_parts.isEmpty() + ? url_template.arg(url.scheme(), + url.host(), + QStringList({ "...", last_path_segment }).join('/'), + url.query()) + : url_template.arg(url.scheme(), + url.host(), + QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), + url.query()); + + // remove url parts one by one if it's still too long + while (url_compact.length() > max_len && url_path_parts.size() >= 1) + { + url_path_parts.removeLast(); // drop the next to last path segment + url_compact = + url_path_parts.isEmpty() + ? url_template.arg(url.scheme(), + url.host(), + QStringList({ "...", last_path_segment }).join('/'), + url.query()) + : url_template.arg(url.scheme(), + url.host(), + QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), + url.query()); + } + + if ((url_compact.length() >= max_len) && hard_limit) + { + // still too long, truncate normally + url_compact = QString(str_url); + auto to_remove = url_compact.length() - max_len + 3; + url_compact.remove(url_compact.length() - to_remove - 1, to_remove); + url_compact.append("..."); + } + + return url_compact; +} + +static const QStringList s_units_si{ "KB", "MB", "GB", "TB" }; +static const QStringList s_units_kibi{ "KiB", "MiB", "GiB", "TiB" }; + +QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points) +{ + const QStringList units = use_si ? s_units_si : s_units_kibi; + const int scale = use_si ? 1000 : 1024; + + int u = -1; + double r = pow(10, decimal_points); + + do + { + bytes /= scale; + u++; + } + while (round(abs(bytes) * r) / r >= scale && u < units.length() - 1); + + return QString::number(bytes, 'f', 2) + " " + units[u]; +} + +QString StringUtils::getRandomAlphaNumeric() +{ + return QUuid::createUuid().toString(QUuid::Id128); +} + +QPair StringUtils::splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs) +{ + QString left, right; + auto index = s.indexOf(sep, 0, cs); + left = s.mid(0, index); + right = s.mid(index + sep.length()); + return qMakePair(left, right); +} + +QPair StringUtils::splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs) +{ + QString left, right; + auto index = s.indexOf(sep, 0, cs); + left = s.mid(0, index); + right = s.mid(left.length() + 1); + return qMakePair(left, right); +} + +QPair StringUtils::splitFirst(const QString& s, const QRegularExpression& re) +{ + QString left, right; + QRegularExpressionMatch match; + auto index = s.indexOf(re, 0, &match); + left = s.mid(0, index); + auto end = match.hasMatch() ? left.length() + match.capturedLength() : left.length() + 1; + right = s.mid(end); + return qMakePair(left, right); +} + +QString StringUtils::htmlListPatch(QString htmlStr) +{ + static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>"); + int pos = htmlStr.indexOf(s_ulMatcher); + int imgPos; + while (pos != -1) + { + pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index + imgPos = htmlStr.indexOf(""); + + pos = htmlStr.indexOf(s_ulMatcher, pos); + } + return htmlStr; +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/StringUtils.h b/archived/projt-launcher/launcher/StringUtils.h new file mode 100644 index 0000000000..e0fdf595cf --- /dev/null +++ b/archived/projt-launcher/launcher/StringUtils.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include + +namespace StringUtils +{ + +#if defined Q_OS_WIN32 + using string = std::wstring; + + inline string toStdString(QString s) + { + return s.toStdWString(); + } + inline QString fromStdString(string s) + { + return QString::fromStdWString(s); + } +#else + using string = std::string; + + inline string toStdString(QString s) + { + return s.toStdString(); + } + inline QString fromStdString(string s) + { + return QString::fromStdString(s); + } +#endif + + int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs); + + /** + * @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path + * @param url Url to truncate + * @param max_len max length of url in characters + * @param hard_limit if truncating the path can't get the url short enough, truncate it normally. + */ + QString truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit = false); + + QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1); + + QString getRandomAlphaNumeric(); + + QPair splitFirst(const QString& s, + const QString& sep, + Qt::CaseSensitivity cs = Qt::CaseSensitive); + QPair splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); + QPair splitFirst(const QString& s, const QRegularExpression& re); + + QString htmlListPatch(QString htmlStr); + +} // namespace StringUtils diff --git a/archived/projt-launcher/launcher/SysInfo.cpp b/archived/projt-launcher/launcher/SysInfo.cpp new file mode 100644 index 0000000000..1203e6d5bd --- /dev/null +++ b/archived/projt-launcher/launcher/SysInfo.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "SysInfo.h" + +#include +#include + +#include "HardwareInfo.h" + +#ifdef Q_OS_MACOS +#include + +bool rosettaDetect() +{ + int ret = 0; + size_t size = sizeof(ret); + if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) + { + return false; + } + return ret == 1; +} +#endif + +namespace SysInfo +{ + QString currentSystem() + { +#if defined(Q_OS_LINUX) + return "linux"; +#elif defined(Q_OS_MACOS) + return "osx"; +#elif defined(Q_OS_WINDOWS) + return "windows"; +#elif defined(Q_OS_FREEBSD) + return "freebsd"; +#elif defined(Q_OS_OPENBSD) + return "openbsd"; +#else + return "unknown"; +#endif + } + + QString useQTForArch() + { +#if defined(Q_OS_MACOS) && !defined(Q_PROCESSOR_ARM) + if (rosettaDetect()) + { + return "arm64"; + } + else + { + return "x86_64"; + } +#endif + return QSysInfo::currentCpuArchitecture(); + } + + int defaultMaxJvmMem() + { + // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB + if (const uint64_t totalRAM = HardwareInfo::totalRamMiB(); totalRAM < (4096 * 1.5)) + return totalRAM / 1.5; + else + return 4096; + } + + QString getSupportedJavaArchitecture() + { + auto sys = currentSystem(); + auto arch = useQTForArch(); + if (sys == "windows") + { + if (arch == "x86_64") + return "windows-x64"; + if (arch == "i386") + return "windows-x86"; + // Unknown, maybe arm, appending arch + return "windows-" + arch; + } + if (sys == "osx") + { + if (arch == "arm64") + return "mac-os-arm64"; + if (arch.contains("64")) + return "mac-os-x64"; + if (arch.contains("86")) + return "mac-os-x86"; + // Unknown, maybe something new, appending arch + return "mac-os-" + arch; + } + else if (sys == "linux") + { + if (arch == "x86_64") + return "linux-x64"; + if (arch == "i386") + return "linux-x86"; + // will work for arm32 arm(64) + return "linux-" + arch; + } + return {}; + } +} // namespace SysInfo diff --git a/archived/projt-launcher/launcher/SysInfo.h b/archived/projt-launcher/launcher/SysInfo.h new file mode 100644 index 0000000000..19c48aed22 --- /dev/null +++ b/archived/projt-launcher/launcher/SysInfo.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +namespace SysInfo +{ + QString currentSystem(); + QString useQTForArch(); + QString getSupportedJavaArchitecture(); + int defaultMaxJvmMem(); + + inline int suitableMaxMem() + { + return defaultMaxJvmMem(); + } +} // namespace SysInfo diff --git a/archived/projt-launcher/launcher/Untar.cpp b/archived/projt-launcher/launcher/Untar.cpp new file mode 100644 index 0000000000..df575c9d4a --- /dev/null +++ b/archived/projt-launcher/launcher/Untar.cpp @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ +#include "Untar.h" +#include +#include +#include +#include +#include +#include "FileSystem.h" + +// adaptation of the: +// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c +// - https://en.wikipedia.org/wiki/Tar_(computing) +// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp + +#define BLOCKSIZE 512 +#define SHORTNAMESIZE 100 + +enum class TypeFlag : char +{ + Regular = '0', // regular file + ARegular = 0, // regular file + Link = '1', // link + Symlink = '2', // reserved + Character = '3', // character special + Block = '4', // block special + Directory = '5', // directory + FIFO = '6', // FIFO special + Contiguous = '7', // reserved + // Posix stuff + GlobalPosixHeader = 'g', + ExtendedPosixHeader = 'x', + // 'A'– 'Z' Vendor specific extensions(POSIX .1 - 1988) + // GNU + GNULongLink = 'K', /* long link name */ + GNULongName = 'L', /* long file name */ +}; + +// struct Header { /* byte offset */ +// char name[100]; /* 0 */ +// char mode[8]; /* 100 */ +// char uid[8]; /* 108 */ +// char gid[8]; /* 116 */ +// char size[12]; /* 124 */ +// char mtime[12]; /* 136 */ +// char chksum[8]; /* 148 */ +// TypeFlag typeflag; /* 156 */ +// char linkname[100]; /* 157 */ +// char magic[6]; /* 257 */ +// char version[2]; /* 263 */ +// char uname[32]; /* 265 */ +// char gname[32]; /* 297 */ +// char devmajor[8]; /* 329 */ +// char devminor[8]; /* 337 */ +// char prefix[155]; /* 345 */ +// /* 500 */ +// }; + +bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink) +{ + qint64 n = 0; + size--; // ignore trailing null + if (size < 0) + { + qCritical() << "The filename size is negative"; + return false; + } + longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE + for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) + { + n = in->read(longlink.data() + offset, BLOCKSIZE); + if (n != BLOCKSIZE) + { + qCritical() << "The expected blocksize was not respected for the name"; + return false; + } + } + longlink.truncate(qstrlen(longlink.constData())); + return true; +} + +int getOctal(char* buffer, int maxlenght, bool* ok) +{ + return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8); +} + +QString decodeName(char* name) +{ + return QFile::decodeName(QByteArray(name, qstrnlen(name, 100))); +} +bool Tar::extract(QIODevice* in, QString dst) +{ + char buffer[BLOCKSIZE]; + QString name, symlink, firstFolderName; + bool doNotReset = false, ok; + while (true) + { + auto n = in->read(buffer, BLOCKSIZE); + if (n != BLOCKSIZE) + { // allways expect complete blocks + qCritical() << "The expected blocksize was not respected"; + return false; + } + if (buffer[0] == 0) + { // end of archive + return true; + } + int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read + // permisions + if (!ok) + { + qCritical() << "The file mode can't be read"; + return false; + } + // there are names that are exactly 100 bytes long + // and neither longlink nor \0 terminated (bug:101472) + + if (name.isEmpty()) + { + name = decodeName(buffer); + if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) + { + name = name.mid(firstFolderName.size()); + } + } + if (symlink.isEmpty()) + symlink = decodeName(buffer); + qint64 size = getOctal(buffer + 124, 12, &ok); + if (!ok) + { + qCritical() << "The file size can't be read"; + return false; + } + switch (TypeFlag(buffer[156])) + { + case TypeFlag::Regular: + /* fallthrough */ + case TypeFlag::ARegular: + { + auto fileName = FS::PathCombine(dst, name); + if (!FS::ensureFilePathExists(fileName)) + { + qCritical() << "Can't ensure the file path to exist: " << fileName; + return false; + } + QFile out(fileName); + if (!out.open(QFile::WriteOnly)) + { + qCritical() << "Can't open file:" << fileName; + return false; + } + out.setPermissions(QFile::Permissions(mode)); + while (size > 0) + { + QByteArray tmp(BLOCKSIZE, 0); + n = in->read(tmp.data(), BLOCKSIZE); + if (n != BLOCKSIZE) + { + qCritical() << "The expected blocksize was not respected when reading file"; + return false; + } + tmp.truncate(qMin(qint64(BLOCKSIZE), size)); + out.write(tmp); + size -= BLOCKSIZE; + } + break; + } + case TypeFlag::Directory: + { + if (firstFolderName.isEmpty()) + { + firstFolderName = name; + break; + } + auto folderPath = FS::PathCombine(dst, name); + if (!FS::ensureFolderPathExists(folderPath)) + { + qCritical() << "Can't ensure that folder exists: " << folderPath; + return false; + } + break; + } + case TypeFlag::GNULongLink: + { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) + { + symlink = QFile::decodeName(longlink.constData()); + } + else + { + qCritical() << "Failed to read long link"; + return false; + } + break; + } + case TypeFlag::GNULongName: + { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) + { + name = QFile::decodeName(longlink.constData()); + } + else + { + qCritical() << "Failed to read long name"; + return false; + } + break; + } + case TypeFlag::Link: + /* fallthrough */ + case TypeFlag::Symlink: + { + auto fileName = FS::PathCombine(dst, name); + if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) + { // do not use symlinks + qCritical() << "Can't create link for:" << fileName + << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink); + return false; + } + FS::ensureFilePathExists(fileName); + QFile::setPermissions(fileName, QFile::Permissions(mode)); + break; + } + case TypeFlag::Character: + /* fallthrough */ + case TypeFlag::Block: + /* fallthrough */ + case TypeFlag::FIFO: + /* fallthrough */ + case TypeFlag::Contiguous: + /* fallthrough */ + case TypeFlag::GlobalPosixHeader: + /* fallthrough */ + case TypeFlag::ExtendedPosixHeader: + /* fallthrough */ + default: break; + } + if (!doNotReset) + { + name.truncate(0); + symlink.truncate(0); + } + doNotReset = false; + } + return true; +} + +bool GZTar::extract(QString src, QString dst) +{ + QuaGzipFile a(src); + if (!a.open(QIODevice::ReadOnly)) + { + qCritical() << "Can't open tar file:" << src; + return false; + } + return Tar::extract(&a, dst); +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/Untar.h b/archived/projt-launcher/launcher/Untar.h new file mode 100644 index 0000000000..912cce28b7 --- /dev/null +++ b/archived/projt-launcher/launcher/Untar.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ +#pragma once +#include + +// this is a hack used for the java downloader (feel free to remove it in favor of a library) +// both extract functions will extract the first folder inside dest(disregarding the prefix) +namespace Tar +{ + bool extract(QIODevice* in, QString dst); +} + +namespace GZTar +{ + bool extract(QString src, QString dst); +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/Usable.h b/archived/projt-launcher/launcher/Usable.h new file mode 100644 index 0000000000..20912a3572 --- /dev/null +++ b/archived/projt-launcher/launcher/Usable.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +#include "QObjectPtr.h" + +class Usable; + +/** + * Base class for things that can be used by multiple other things and we want to track the use count. + * + * @see UseLock + */ +class Usable +{ + friend class UseLock; + + public: + virtual ~Usable() + {} + + std::size_t useCount() const + { + return m_useCount; + } + bool isInUse() const + { + return m_useCount > 0; + } + + protected: + virtual void decrementUses() + { + m_useCount--; + } + virtual void incrementUses() + { + m_useCount++; + } + + private: + std::size_t m_useCount = 0; +}; + +/** + * Lock class to use for keeping track of uses of other things derived from Usable + * + * @see Usable + */ +class UseLock +{ + public: + UseLock(shared_qobject_ptr usable) : m_usable(usable) + { + // this doesn't use shared pointer use count, because that wouldn't be correct. this count is separate. + m_usable->incrementUses(); + } + ~UseLock() + { + m_usable->decrementUses(); + } + + private: + shared_qobject_ptr m_usable; +}; diff --git a/archived/projt-launcher/launcher/Version.cpp b/archived/projt-launcher/launcher/Version.cpp new file mode 100644 index 0000000000..30047aa455 --- /dev/null +++ b/archived/projt-launcher/launcher/Version.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "Version.h" + +#include +#include +#include + +Version::Version(QString str) : m_string(std::move(str)) +{ + parse(); +} + +#define VERSION_OPERATOR(return_on_different) \ + bool exclude_our_sections = false; \ + bool exclude_their_sections = false; \ + \ + const auto size = qMax(m_sections.size(), other.m_sections.size()); \ + for (int i = 0; i < size; ++i) \ + { \ + Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \ + Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \ + \ + { /* Don't include appendixes in the comparison */ \ + if (sec1.isAppendix()) \ + exclude_our_sections = true; \ + if (sec2.isAppendix()) \ + exclude_their_sections = true; \ + \ + if (exclude_our_sections) \ + { \ + sec1 = Section(); \ + if (sec2.m_isNull) \ + break; \ + } \ + \ + if (exclude_their_sections) \ + { \ + sec2 = Section(); \ + if (sec1.m_isNull) \ + break; \ + } \ + } \ + \ + if (sec1 != sec2) \ + return return_on_different; \ + } + +bool Version::operator<(const Version& other) const +{ + VERSION_OPERATOR(sec1 < sec2) + + return false; +} +bool Version::operator==(const Version& other) const +{ + VERSION_OPERATOR(false) + + return true; +} +bool Version::operator!=(const Version& other) const +{ + return !operator==(other); +} +bool Version::operator<=(const Version& other) const +{ + return *this < other || *this == other; +} +bool Version::operator>(const Version& other) const +{ + return !(*this <= other); +} +bool Version::operator>=(const Version& other) const +{ + return !(*this < other); +} + +void Version::parse() +{ + m_sections.clear(); + QString currentSection; + + if (m_string.isEmpty()) + return; + + auto classChange = [¤tSection](QChar lastChar, QChar currentChar) + { + if (lastChar.isNull()) + return false; + if (lastChar.isDigit() != currentChar.isDigit()) + return true; + + const QList s_separators{ '.', '-', '+' }; + if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar) + return true; + + return false; + }; + + currentSection += m_string.at(0); + for (int i = 1; i < m_string.size(); ++i) + { + const auto& current_char = m_string.at(i); + if (classChange(m_string.at(i - 1), current_char)) + { + if (!currentSection.isEmpty()) + m_sections.append(Section(currentSection)); + currentSection = ""; + } + + currentSection += current_char; + } + + if (!currentSection.isEmpty()) + m_sections.append(Section(currentSection)); +} + +/// qDebug print support for the Version class +QDebug operator<<(QDebug debug, const Version& v) +{ + QDebugStateSaver saver(debug); + + debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; + + bool first = true; + for (auto s : v.m_sections) + { + if (!first) + debug.nospace() << ", "; + debug.nospace() << s.m_fullString; + first = false; + } + + debug.nospace() << " ]" + << " }"; + + return debug; +} diff --git a/archived/projt-launcher/launcher/Version.h b/archived/projt-launcher/launcher/Version.h new file mode 100644 index 0000000000..60c314c41b --- /dev/null +++ b/archived/projt-launcher/launcher/Version.h @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include + +class QUrl; + +class Version +{ + public: + Version(QString str); + Version() = default; + + bool operator<(const Version& other) const; + bool operator<=(const Version& other) const; + bool operator>(const Version& other) const; + bool operator>=(const Version& other) const; + bool operator==(const Version& other) const; + bool operator!=(const Version& other) const; + + QString toString() const + { + return m_string; + } + bool isEmpty() const + { + return m_string.isEmpty(); + } + + friend QDebug operator<<(QDebug debug, const Version& v); + + private: + struct Section + { + explicit Section(QString fullString) : m_fullString(std::move(fullString)) + { + qsizetype cutoff = m_fullString.size(); + for (int i = 0; i < m_fullString.size(); i++) + { + if (!m_fullString[i].isDigit()) + { + cutoff = i; + break; + } + } + + auto numPart = QStringView{ m_fullString }.left(cutoff); + + if (!numPart.isEmpty()) + { + m_isNull = false; + m_numPart = numPart.toInt(); + } + + auto stringPart = QStringView{ m_fullString }.mid(cutoff); + + if (!stringPart.isEmpty()) + { + m_isNull = false; + m_stringPart = stringPart.toString(); + } + } + + explicit Section() = default; + + bool m_isNull = true; + + int m_numPart = 0; + QString m_stringPart; + + QString m_fullString; + + inline bool isAppendix() const + { + return m_stringPart.startsWith('+'); + } + inline bool isPreRelease() const + { + return m_stringPart.startsWith('-') && m_stringPart.length() > 1; + } + + inline bool isReleaseCandidate(int* out_rc = nullptr) const + { + if (!isPreRelease()) + return false; + const auto lower = m_stringPart.toLower(); + if (!lower.startsWith("-rc")) + return false; + const auto suffix = lower.mid(3); + if (suffix.isEmpty()) + return false; + bool ok = false; + const auto rc = suffix.toInt(&ok); + if (!ok) + return false; + if (out_rc) + *out_rc = rc; + return true; + } + + inline bool operator==(const Section& other) const + { + if (m_isNull && !other.m_isNull) + return false; + if (!m_isNull && other.m_isNull) + return false; + + if (!m_isNull && !other.m_isNull) + { + return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); + } + + return true; + } + + inline bool operator<(const Section& other) const + { + static auto unequal_is_less = [](Section const& non_null) -> bool + { + if (non_null.m_stringPart.isEmpty()) + return non_null.m_numPart == 0; + return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease(); + }; + + if (!m_isNull && other.m_isNull) + return unequal_is_less(*this); + if (m_isNull && !other.m_isNull) + return !unequal_is_less(other); + + if (!m_isNull && !other.m_isNull) + { + int rc_a = 0; + int rc_b = 0; + const auto is_rc_a = isReleaseCandidate(&rc_a); + const auto is_rc_b = other.isReleaseCandidate(&rc_b); + if (is_rc_a && is_rc_b && rc_a != rc_b) + return rc_a < rc_b; + + if (m_numPart < other.m_numPart) + return true; + if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) + return true; + + if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty()) + return false; + if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty()) + return true; + + return false; + } + + return m_fullString < other.m_fullString; + } + + inline bool operator!=(const Section& other) const + { + return !(*this == other); + } + inline bool operator>(const Section& other) const + { + return !(*this < other || *this == other); + } + }; + + private: + QString m_string; + QList
m_sections; + + void parse(); +}; diff --git a/archived/projt-launcher/launcher/VersionProxyModel.cpp b/archived/projt-launcher/launcher/VersionProxyModel.cpp new file mode 100644 index 0000000000..9596f16a21 --- /dev/null +++ b/archived/projt-launcher/launcher/VersionProxyModel.cpp @@ -0,0 +1,552 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "VersionProxyModel.h" +#include +#include +#include +#include +#include + +class VersionFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + public: + VersionFilterModel(VersionProxyModel* parent) : QSortFilterProxyModel(parent) + { + m_parent = parent; + setSortRole(BaseVersionList::SortRole); + sort(0, Qt::DescendingOrder); + } + + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const + { + const auto& filters = m_parent->filters(); + const QString& search = m_parent->search(); + const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + + if (!search.isEmpty() + && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive)) + return false; + + for (auto it = filters.begin(); it != filters.end(); ++it) + { + auto data = sourceModel()->data(idx, it.key()); + auto match = data.toString(); + if (!it.value()(match)) + { + return false; + } + } + return true; + } + + void filterChanged() + { + invalidateFilter(); + } + + private: + VersionProxyModel* m_parent; +}; + +VersionProxyModel::VersionProxyModel(QObject* parent) : QAbstractProxyModel(parent) +{ + filterModel = new VersionFilterModel(this); + connect(filterModel, &QAbstractItemModel::dataChanged, this, &VersionProxyModel::sourceDataChanged); + connect(filterModel, + &QAbstractItemModel::rowsAboutToBeInserted, + this, + &VersionProxyModel::sourceRowsAboutToBeInserted); + connect(filterModel, &QAbstractItemModel::rowsInserted, this, &VersionProxyModel::sourceRowsInserted); + connect(filterModel, + &QAbstractItemModel::rowsAboutToBeRemoved, + this, + &VersionProxyModel::sourceRowsAboutToBeRemoved); + connect(filterModel, &QAbstractItemModel::rowsRemoved, this, &VersionProxyModel::sourceRowsRemoved); + connect(filterModel, + QOverload&, QAbstractItemModel::LayoutChangeHint>::of( + &QAbstractItemModel::layoutAboutToBeChanged), + this, + &VersionProxyModel::sourceLayoutAboutToBeChanged); + + connect(filterModel, + QOverload&, QAbstractItemModel::LayoutChangeHint>::of( + &QAbstractItemModel::layoutChanged), + this, + &VersionProxyModel::sourceLayoutChanged); + + connect(filterModel, &QAbstractItemModel::modelAboutToBeReset, this, &VersionProxyModel::sourceAboutToBeReset); + connect(filterModel, &QAbstractItemModel::modelReset, this, &VersionProxyModel::sourceReset); + + QAbstractProxyModel::setSourceModel(filterModel); +} + +QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (section < 0 || section >= m_columns.size()) + return QVariant(); + if (orientation != Qt::Horizontal) + return QVariant(); + auto column = m_columns[section]; + if (role == Qt::DisplayRole) + { + switch (column) + { + case Name: return tr("Version"); + case ParentVersion: return m_parentVersionName; // From metadata + case Branch: return tr("Branch"); + case Type: return tr("Type"); + case CPUArchitecture: return tr("Architecture"); + case Path: return tr("Path"); + case JavaName: return tr("Java Name"); + case JavaMajor: return tr("Major Version"); + case Time: return tr("Released"); + } + } + else if (role == Qt::ToolTipRole) + { + switch (column) + { + case Name: return tr("The name of the version."); + case ParentVersion: return tr("%1 version").arg(m_parentVersionName); // From metadata + case Branch: return tr("The version's branch"); + case Type: return tr("The version's type"); + case CPUArchitecture: return tr("CPU Architecture"); + case Path: return tr("Filesystem path to this version"); + case JavaName: return tr("The alternative name of the Java version"); + case JavaMajor: return tr("The Java major version"); + case Time: return tr("Release date of this version"); + } + } + return QVariant(); +} + +QVariant VersionProxyModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + { + return QVariant(); + } + auto column = m_columns[index.column()]; + auto parentIndex = mapToSource(index); + switch (role) + { + case Qt::DisplayRole: + { + switch (column) + { + case Name: + { + QString version = sourceModel()->data(parentIndex, BaseVersionList::VersionRole).toString(); + if (version == m_currentVersion) + { + return tr("%1 (installed)").arg(version); + } + return version; + } + case ParentVersion: return sourceModel()->data(parentIndex, BaseVersionList::ParentVersionRole); + case Branch: return sourceModel()->data(parentIndex, BaseVersionList::BranchRole); + case Type: return sourceModel()->data(parentIndex, BaseVersionList::TypeRole); + case CPUArchitecture: return sourceModel()->data(parentIndex, BaseVersionList::CPUArchitectureRole); + case Path: return sourceModel()->data(parentIndex, BaseVersionList::PathRole); + case JavaName: return sourceModel()->data(parentIndex, BaseVersionList::JavaNameRole); + case JavaMajor: return sourceModel()->data(parentIndex, BaseVersionList::JavaMajorRole); + case Time: + return sourceModel()->data(parentIndex, projt::meta::MetaVersionList::TimestampRole).toDate(); + default: return QVariant(); + } + } + case Qt::ToolTipRole: + { + if (column == Name && hasRecommended) + { + auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if (value.toBool()) + { + return tr("Recommended"); + } + else if (hasLatest) + { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) + { + return tr("Latest"); + } + } + } + return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); + } + case Qt::DecorationRole: + { + if (column == Name && hasRecommended) + { + auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if (recommenced.toBool()) + { + return QIcon::fromTheme("star"); + } + else if (hasLatest) + { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) + { + return QIcon::fromTheme("bug"); + } + } + QPixmap pixmap; + QPixmapCache::find("placeholder", &pixmap); + if (!pixmap) + { + QPixmap px(16, 16); + px.fill(Qt::transparent); + QPixmapCache::insert("placeholder", px); + return px; + } + return pixmap; + } + return QVariant(); + } + default: + { + if (roles.contains((BaseVersionList::ModelRoles)role)) + { + return sourceModel()->data(parentIndex, role); + } + return QVariant(); + } + } +} + +QModelIndex VersionProxyModel::parent([[maybe_unused]] const QModelIndex& child) const +{ + return QModelIndex(); +} + +QModelIndex VersionProxyModel::mapFromSource(const QModelIndex& sourceIndex) const +{ + if (sourceIndex.isValid()) + { + return index(sourceIndex.row(), 0); + } + return QModelIndex(); +} + +QModelIndex VersionProxyModel::mapToSource(const QModelIndex& proxyIndex) const +{ + if (proxyIndex.isValid()) + { + return sourceModel()->index(proxyIndex.row(), 0); + } + return QModelIndex(); +} + +QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex& parent) const +{ + // no trees here... shoo + if (parent.isValid()) + { + return QModelIndex(); + } + if (row < 0 || row >= sourceModel()->rowCount()) + return QModelIndex(); + if (column < 0 || column >= columnCount()) + return QModelIndex(); + return QAbstractItemModel::createIndex(row, column); +} + +int VersionProxyModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_columns.size(); +} + +int VersionProxyModel::rowCount(const QModelIndex& parent) const +{ + if (sourceModel()) + { + return sourceModel()->rowCount(parent); + } + return 0; +} + +void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, const QModelIndex& source_bottom_right) +{ + if (source_top_left.parent() != source_bottom_right.parent()) + return; + + // whole row is getting changed + auto topLeft = createIndex(source_top_left.row(), 0); + auto bottomRight = createIndex(source_bottom_right.row(), columnCount() - 1); + emit dataChanged(topLeft, bottomRight); +} + +void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) +{ + auto replacing = dynamic_cast(replacingRaw); + + m_columns.clear(); + if (!replacing) + { + roles.clear(); + filterModel->setSourceModel(replacing); + return; + } + + // Extract parent version name from metadata if available + auto versionList = dynamic_cast(replacing); + if (versionList) + { + m_parentVersionName = versionList->displayName(); + } + else + { + m_parentVersionName = tr("Minecraft"); // Fallback + } + + roles = replacing->providesRoles(); + if (roles.contains(BaseVersionList::VersionRole)) + { + m_columns.push_back(Name); + } + /* + if(roles.contains(BaseVersionList::ParentVersionRole)) + { + m_columns.push_back(ParentVersion); + } + */ + if (roles.contains(BaseVersionList::CPUArchitectureRole)) + { + m_columns.push_back(CPUArchitecture); + } + if (roles.contains(BaseVersionList::PathRole)) + { + m_columns.push_back(Path); + } + if (roles.contains(BaseVersionList::JavaNameRole)) + { + m_columns.push_back(JavaName); + } + if (roles.contains(BaseVersionList::JavaMajorRole)) + { + m_columns.push_back(JavaMajor); + } + if (roles.contains(projt::meta::MetaVersionList::TimestampRole)) + { + m_columns.push_back(Time); + } + if (roles.contains(BaseVersionList::BranchRole)) + { + m_columns.push_back(Branch); + } + if (roles.contains(BaseVersionList::TypeRole)) + { + m_columns.push_back(Type); + } + if (roles.contains(BaseVersionList::RecommendedRole)) + { + hasRecommended = true; + } + if (roles.contains(BaseVersionList::LatestRole)) + { + hasLatest = true; + } + filterModel->setSourceModel(replacing); +} + +QModelIndex VersionProxyModel::getRecommended() const +{ + if (!roles.contains(BaseVersionList::RecommendedRole)) + { + return index(0, 0); + } + int recommended = 0; + for (int i = 0; i < rowCount(); i++) + { + auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::RecommendedRole); + if (value.toBool()) + { + recommended = i; + } + } + return index(recommended, 0); +} + +QModelIndex VersionProxyModel::getVersion(const QString& version) const +{ + int found = -1; + for (int i = 0; i < rowCount(); i++) + { + auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::VersionRole); + if (value.toString() == version) + { + found = i; + } + } + if (found == -1) + { + return QModelIndex(); + } + return index(found, 0); +} + +void VersionProxyModel::clearFilters() +{ + m_filters.clear(); + m_search.clear(); + filterModel->filterChanged(); +} + +void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter f) +{ + m_filters[column] = std::move(f); + filterModel->filterChanged(); +} + +void VersionProxyModel::setSearch(const QString& search) +{ + m_search = search; + filterModel->filterChanged(); +} + +const VersionProxyModel::FilterMap& VersionProxyModel::filters() const +{ + return m_filters; +} + +const QString& VersionProxyModel::search() const +{ + return m_search; +} + +void VersionProxyModel::sourceAboutToBeReset() +{ + beginResetModel(); +} + +void VersionProxyModel::sourceReset() +{ + endResetModel(); +} + +void VersionProxyModel::sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last) +{ + beginInsertRows(parent, first, last); +} + +void VersionProxyModel::sourceRowsInserted([[maybe_unused]] const QModelIndex& parent, + [[maybe_unused]] int first, + [[maybe_unused]] int last) +{ + endInsertRows(); +} + +void VersionProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +{ + beginRemoveRows(parent, first, last); +} + +void VersionProxyModel::sourceRowsRemoved([[maybe_unused]] const QModelIndex& parent, + [[maybe_unused]] int first, + [[maybe_unused]] int last) +{ + endRemoveRows(); +} + +void VersionProxyModel::sourceRowsAboutToBeMoved(const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destinationParent, + int destinationRow) +{ + beginMoveRows(sourceParent, sourceStart, sourceEnd, destinationParent, destinationRow); +} + +void VersionProxyModel::sourceRowsMoved(const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destinationParent, + int destinationRow) +{ + Q_UNUSED(sourceParent); + Q_UNUSED(sourceStart); + Q_UNUSED(sourceEnd); + Q_UNUSED(destinationParent); + Q_UNUSED(destinationRow); + endMoveRows(); +} + +void VersionProxyModel::sourceLayoutAboutToBeChanged(const QList& sourceParents, + QAbstractItemModel::LayoutChangeHint hint) +{ + emit layoutAboutToBeChanged(sourceParents, hint); +} + +void VersionProxyModel::sourceLayoutChanged(const QList& sourceParents, + QAbstractItemModel::LayoutChangeHint hint) +{ + emit layoutChanged(sourceParents, hint); +} + +void VersionProxyModel::setCurrentVersion(const QString& version) +{ + m_currentVersion = version; +} + +#include "VersionProxyModel.moc" diff --git a/archived/projt-launcher/launcher/VersionProxyModel.h b/archived/projt-launcher/launcher/VersionProxyModel.h new file mode 100644 index 0000000000..5e3a3f5487 --- /dev/null +++ b/archived/projt-launcher/launcher/VersionProxyModel.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include "BaseVersionList.h" + +#include + +class VersionFilterModel; + +class VersionProxyModel : public QAbstractProxyModel +{ + Q_OBJECT + public: + enum Column + { + Name, + ParentVersion, + Branch, + Type, + CPUArchitecture, + Path, + Time, + JavaName, + JavaMajor + }; + using FilterMap = QHash; + + public: + VersionProxyModel(QObject* parent = 0); + virtual ~VersionProxyModel() {}; + + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; + virtual QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; + virtual QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + virtual QModelIndex parent(const QModelIndex& child) const override; + virtual void setSourceModel(QAbstractItemModel* sourceModel) override; + + const FilterMap& filters() const; + const QString& search() const; + void setFilter(BaseVersionList::ModelRoles column, Filter filter); + void setSearch(const QString& search); + void clearFilters(); + QModelIndex getRecommended() const; + QModelIndex getVersion(const QString& version) const; + void setCurrentVersion(const QString& version); + private slots: + + void sourceDataChanged(const QModelIndex& source_top_left, const QModelIndex& source_bottom_right); + + void sourceAboutToBeReset(); + void sourceReset(); + + void sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last); + void sourceRowsInserted(const QModelIndex& parent, int first, int last); + + void sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); + void sourceRowsRemoved(const QModelIndex& parent, int first, int last); + + void sourceRowsAboutToBeMoved(const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destinationParent, + int destinationRow); + void sourceRowsMoved(const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destinationParent, + int destinationRow); + + void sourceLayoutAboutToBeChanged(const QList& sourceParents, + QAbstractItemModel::LayoutChangeHint hint); + void sourceLayoutChanged(const QList& sourceParents, + QAbstractItemModel::LayoutChangeHint hint); + + private: + QList m_columns; + FilterMap m_filters; + QString m_search; + BaseVersionList::RoleList roles; + VersionFilterModel* filterModel; + bool hasRecommended = false; + bool hasLatest = false; + QString m_currentVersion; + QString m_parentVersionName = tr("Minecraft"); // Default, updated from metadata +}; diff --git a/archived/projt-launcher/launcher/WatchLock.h b/archived/projt-launcher/launcher/WatchLock.h new file mode 100644 index 0000000000..c8f2401636 --- /dev/null +++ b/archived/projt-launcher/launcher/WatchLock.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +struct WatchLock +{ + WatchLock(QFileSystemWatcher* watcher, const QString& directory) : m_watcher(watcher), m_directory(directory) + { + m_watcher->removePath(m_directory); + } + ~WatchLock() + { + m_watcher->addPath(m_directory); + } + QFileSystemWatcher* m_watcher; + QString m_directory; +}; diff --git a/archived/projt-launcher/launcher/console/Console.hpp b/archived/projt-launcher/launcher/console/Console.hpp new file mode 100644 index 0000000000..89e80bfbfa --- /dev/null +++ b/archived/projt-launcher/launcher/console/Console.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#endif + +namespace projt::console +{ + + inline bool isConsole() + { +#if defined Q_OS_WIN32 + HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + if (handle == INVALID_HANDLE_VALUE || handle == nullptr) + { + return false; + } + DWORD mode = 0; + return GetConsoleMode(handle, &mode) != 0; +#else + return isatty(fileno(stdout)) != 0; +#endif + } + +} // namespace console diff --git a/archived/projt-launcher/launcher/console/ConsoleStub.cpp b/archived/projt-launcher/launcher/console/ConsoleStub.cpp new file mode 100644 index 0000000000..11da24b283 --- /dev/null +++ b/archived/projt-launcher/launcher/console/ConsoleStub.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef _WIN32 +extern "C" int projt_console_stub(void) +{ + return 0; +} +#endif diff --git a/archived/projt-launcher/launcher/console/WindowsConsole.cpp b/archived/projt-launcher/launcher/console/WindowsConsole.cpp new file mode 100644 index 0000000000..ecc2b3b7d2 --- /dev/null +++ b/archived/projt-launcher/launcher/console/WindowsConsole.cpp @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "WindowsConsole.hpp" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include +#include +#include +#include + +namespace +{ + bool ShouldBindStdHandle(DWORD handleId) + { + HANDLE handle = GetStdHandle(handleId); + if (handle == INVALID_HANDLE_VALUE || handle == nullptr) + { + return true; + } + DWORD type = GetFileType(handle); + return type == FILE_TYPE_CHAR || type == FILE_TYPE_UNKNOWN; + } + + void RedirectHandle(DWORD handleId, FILE* stream, const char* mode) + { + HANDLE stdHandle = GetStdHandle(handleId); + if (stdHandle == INVALID_HANDLE_VALUE || stdHandle == nullptr) + { + return; + } + + int fileDescriptor = _open_osfhandle(reinterpret_cast(stdHandle), _O_TEXT); + if (fileDescriptor == -1) + { + return; + } + + FILE* file = _fdopen(fileDescriptor, mode); + if (file == nullptr) + { + return; + } + + if (_dup2(_fileno(file), _fileno(stream)) == 0) + { + setvbuf(stream, nullptr, _IONBF, 0); + } + } + + void ReopenToNull(FILE* stream, const char* mode) + { + FILE* dummyFile = nullptr; + freopen_s(&dummyFile, "nul", mode, stream); + } +} // namespace + +namespace projt::console +{ + void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr) + { + if (bindStdIn) + { + ReopenToNull(stdin, "r"); + } + if (bindStdOut) + { + ReopenToNull(stdout, "w"); + } + if (bindStdErr) + { + ReopenToNull(stderr, "w"); + } + + if (bindStdIn) + { + RedirectHandle(STD_INPUT_HANDLE, stdin, "r"); + } + if (bindStdOut) + { + RedirectHandle(STD_OUTPUT_HANDLE, stdout, "w"); + } + if (bindStdErr) + { + RedirectHandle(STD_ERROR_HANDLE, stderr, "w"); + } + if (bindStdIn) + { + std::wcin.clear(); + std::cin.clear(); + } + if (bindStdOut) + { + std::wcout.clear(); + std::cout.clear(); + } + if (bindStdErr) + { + std::wcerr.clear(); + std::cerr.clear(); + } + } + + bool AttachWindowsConsole() + { + bool bindStdIn = ShouldBindStdHandle(STD_INPUT_HANDLE); + bool bindStdOut = ShouldBindStdHandle(STD_OUTPUT_HANDLE); + bool bindStdErr = ShouldBindStdHandle(STD_ERROR_HANDLE); + + if (AttachConsole(ATTACH_PARENT_PROCESS)) + { + BindCrtHandlesToStdHandles(bindStdIn, bindStdOut, bindStdErr); + return true; + } + + return false; + } + + std::error_code EnableAnsiSupport() + { + HANDLE console_handle = CreateFileW(L"CONOUT$", + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_SHARE_WRITE | FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (console_handle == INVALID_HANDLE_VALUE) + { + return std::error_code(GetLastError(), std::system_category()); + } + + DWORD console_mode; + if (0 == GetConsoleMode(console_handle, &console_mode)) + { + auto error = std::error_code(GetLastError(), std::system_category()); + CloseHandle(console_handle); + return error; + } + + if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) + { + if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + auto error = std::error_code(GetLastError(), std::system_category()); + CloseHandle(console_handle); + return error; + } + } + + CloseHandle(console_handle); + return {}; + } +} // namespace projt::console diff --git a/archived/projt-launcher/launcher/console/WindowsConsole.hpp b/archived/projt-launcher/launcher/console/WindowsConsole.hpp new file mode 100644 index 0000000000..5f782020db --- /dev/null +++ b/archived/projt-launcher/launcher/console/WindowsConsole.hpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +namespace projt::console +{ + void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); + bool AttachWindowsConsole(); + std::error_code EnableAnsiSupport(); +} // namespace projt::console diff --git a/archived/projt-launcher/launcher/filelink/FileLink.cpp b/archived/projt-launcher/launcher/filelink/FileLink.cpp new file mode 100644 index 0000000000..b32afd4a94 --- /dev/null +++ b/archived/projt-launcher/launcher/filelink/FileLink.cpp @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "FileLink.hpp" + +#include "BuildConfig.h" +#include "StringUtils.h" + +#include +#include +#include + +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include "console/WindowsConsole.hpp" +#endif + +#include +#include + +namespace fs = std::filesystem; + +namespace projt::filelink +{ + FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), m_socket(this) + { +#if defined Q_OS_WIN32 + if (projt::console::AttachWindowsConsole()) + { + m_consoleAttached = true; + } +#endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); + setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); + setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink"); + setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); + + QCommandLineParser parser; + parser.setApplicationDescription(QObject::tr("ProjT Launcher file link helper.")); + parser.addOptions({ { { "s", "server" }, "Join the specified server on launch", "pipe name" }, + { { "H", "hard" }, "Use hard links instead of symlinks", "true/false" } }); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(arguments()); + + const QString serverToJoin = parser.value("server"); + m_useHardLinks = QVariant(parser.value("hard")).toBool(); + + qDebug() << "file link helper launched"; + m_input.setDevice(&m_socket); + + if (!serverToJoin.isEmpty()) + { + qDebug() << "joining server" << serverToJoin; + joinServer(serverToJoin); + m_status = Status::Initialized; + } + else + { + qDebug() << "no server to join"; + m_status = Status::Failed; + } + } + + FileLinkApp::~FileLinkApp() + { + qDebug() << "file link helper shutting down"; + qInstallMessageHandler(nullptr); + +#if defined Q_OS_WIN32 + if (m_consoleAttached) + { + fclose(stdout); + fclose(stdin); + fclose(stderr); + } +#endif + } + + void FileLinkApp::joinServer(const QString& serverName) + { + m_blockSize = 0; + + connect(&m_socket, &QLocalSocket::connected, this, []() { qDebug() << "connected to server"; }); + + connect(&m_socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); + + connect(&m_socket, + &QLocalSocket::errorOccurred, + this, + [this](QLocalSocket::LocalSocketError socketError) + { + m_status = Status::Failed; + switch (socketError) + { + case QLocalSocket::ServerNotFoundError: + qDebug() << ("The host was not found. Please make sure " + "that the server is running and that the " + "server name is correct."); + break; + case QLocalSocket::ConnectionRefusedError: + qDebug() << ("The connection was refused by the peer. " + "Make sure the server is running, " + "and check that the server name " + "is correct."); + break; + case QLocalSocket::PeerClosedError: + qDebug() << ("The connection was closed by the peer. "); + break; + default: qDebug() << "The following error occurred: " << m_socket.errorString(); + } + }); + + connect(&m_socket, + &QLocalSocket::disconnected, + this, + [this]() + { + qDebug() << "disconnected from server, should exit"; + m_status = Status::Succeeded; + exit(); + }); + + m_socket.connectToServer(serverName); + } + + void FileLinkApp::runLink() + { + qDebug() << "creating links"; + + for (const auto& link : m_linksToMake) + { + const QString srcPath = link.src; + const QString dstPath = link.dst; + + FS::ensureFilePathExists(dstPath); + + std::error_code osErr; + if (m_useHardLinks) + { + qDebug() << "making hard link:" << srcPath << "to" << dstPath; + fs::create_hard_link(StringUtils::toStdString(srcPath), StringUtils::toStdString(dstPath), osErr); + } + else if (fs::is_directory(StringUtils::toStdString(srcPath))) + { + qDebug() << "making directory_symlink:" << srcPath << "to" << dstPath; + fs::create_directory_symlink(StringUtils::toStdString(srcPath), + StringUtils::toStdString(dstPath), + osErr); + } + else + { + qDebug() << "making symlink:" << srcPath << "to" << dstPath; + fs::create_symlink(StringUtils::toStdString(srcPath), StringUtils::toStdString(dstPath), osErr); + } + + if (osErr) + { + qWarning() << "Failed to link files:" << QString::fromStdString(osErr.message()); + qDebug() << "Source file:" << srcPath; + qDebug() << "Destination file:" << dstPath; + qDebug() << "Error category:" << osErr.category().name(); + qDebug() << "Error code:" << osErr.value(); + + FS::LinkResult result = { srcPath, dstPath, QString::fromStdString(osErr.message()), osErr.value() }; + m_linkResults.append(result); + } + else + { + FS::LinkResult result = { srcPath, dstPath, "", 0 }; + m_linkResults.append(result); + } + } + + sendResults(); + qDebug() << "done, should exit soon"; + } + + void FileLinkApp::sendResults() + { + QByteArray block; + QDataStream out(&block, QIODevice::WriteOnly); + + quint32 payloadSize = static_cast(sizeof(quint32)); + for (const auto& result : m_linkResults) + { + payloadSize += static_cast(result.src.size()); + payloadSize += static_cast(result.dst.size()); + payloadSize += static_cast(result.err_msg.size()); + payloadSize += static_cast(sizeof(quint32)); + } + qDebug() << "About to write block of size:" << payloadSize; + out << payloadSize; + + out << static_cast(m_linkResults.length()); + for (const auto& result : m_linkResults) + { + out << result.src; + out << result.dst; + out << result.err_msg; + out << static_cast(result.err_value); + } + + qint64 bytesWritten = m_socket.write(block); + bool bytesFlushed = m_socket.flush(); + qDebug() << "block flushed" << bytesWritten << bytesFlushed; + } + + void FileLinkApp::readPathPairs() + { + m_linksToMake.clear(); + qDebug() << "Reading path pairs from server"; + qDebug() << "bytes available" << m_socket.bytesAvailable(); + + if (m_blockSize == 0) + { + if (m_socket.bytesAvailable() < static_cast(sizeof(quint32))) + { + return; + } + qDebug() << "reading block size"; + m_input >> m_blockSize; + } + + qDebug() << "blocksize is" << m_blockSize; + qDebug() << "bytes available" << m_socket.bytesAvailable(); + if (m_socket.bytesAvailable() < static_cast(m_blockSize) || m_input.atEnd()) + { + return; + } + + quint32 numLinks = 0; + m_input >> numLinks; + qDebug() << "numLinks" << numLinks; + + for (quint32 i = 0; i < numLinks; ++i) + { + FS::LinkPair pair; + m_input >> pair.src; + m_input >> pair.dst; + qDebug() << "link" << pair.src << "to" << pair.dst; + m_linksToMake.append(pair); + } + + runLink(); + } +} // namespace projt::filelink diff --git a/archived/projt-launcher/launcher/filelink/FileLink.hpp b/archived/projt-launcher/launcher/filelink/FileLink.hpp new file mode 100644 index 0000000000..b6a21085a3 --- /dev/null +++ b/archived/projt-launcher/launcher/filelink/FileLink.hpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "FileSystem.h" + +namespace projt::filelink +{ + class FileLinkApp final : public QCoreApplication + { + Q_OBJECT + public: + enum class Status + { + Starting, + Failed, + Succeeded, + Initialized + }; + + explicit FileLinkApp(int& argc, char** argv); + ~FileLinkApp() override; + + Status status() const noexcept + { + return m_status; + } + + private: + void joinServer(const QString& serverName); + void readPathPairs(); + void runLink(); + void sendResults(); + + Status m_status = Status::Starting; + bool m_useHardLinks = false; + + QLocalSocket m_socket; + QDataStream m_input; + quint32 m_blockSize = 0; + + QList m_linksToMake; + QList m_linkResults; + +#if defined Q_OS_WIN32 + bool m_consoleAttached = false; +#endif + }; +} // namespace projt::filelink diff --git a/archived/projt-launcher/launcher/filelink/filelink.exe.manifest b/archived/projt-launcher/launcher/filelink/filelink.exe.manifest new file mode 100644 index 0000000000..239aa97835 --- /dev/null +++ b/archived/projt-launcher/launcher/filelink/filelink.exe.manifest @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/filelink/filelink_main.cpp b/archived/projt-launcher/launcher/filelink/filelink_main.cpp new file mode 100644 index 0000000000..8fc7f10774 --- /dev/null +++ b/archived/projt-launcher/launcher/filelink/filelink_main.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "FileLink.hpp" + +int main(int argc, char* argv[]) +{ + projt::filelink::FileLinkApp app(argc, argv); + using Status = projt::filelink::FileLinkApp::Status; + + switch (app.status()) + { + case Status::Starting: + case Status::Initialized: + { + return app.exec(); + } + case Status::Failed: return 1; + case Status::Succeeded: return 0; + default: return -1; + } +} diff --git a/archived/projt-launcher/launcher/icons/IconEntry.cpp b/archived/projt-launcher/launcher/icons/IconEntry.cpp new file mode 100644 index 0000000000..d335ad5b7b --- /dev/null +++ b/archived/projt-launcher/launcher/icons/IconEntry.cpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "IconEntry.hpp" + +namespace projt::icons +{ + IconEntry::ThemeIconProvider IconEntry::s_themeProvider = [](const QString& key) { return QIcon::fromTheme(key); }; + + void IconEntry::setThemeIconProvider(ThemeIconProvider provider) + { + s_themeProvider = provider; + } + + IconType operator--(IconType& t, int) + { + IconType temp = t; + switch (t) + { + case IconType::Builtin: t = IconType::ToBeDeleted; break; + case IconType::Transient: t = IconType::Builtin; break; + case IconType::FileBased: t = IconType::Transient; break; + default: break; + } + return temp; + } + + IconType IconEntry::type() const + { + return m_current_type; + } + + QString IconEntry::name() const + { + if (!m_name.isEmpty()) + { + return m_name; + } + return m_key; + } + + bool IconEntry::has(IconType _type) const + { + return m_images[_type].present(); + } + + QIcon IconEntry::icon() const + { + if (m_current_type == IconType::ToBeDeleted) + { + return QIcon(); + } + auto& icon = m_images[m_current_type].icon; + if (!icon.isNull()) + { + return icon; + } + + if (s_themeProvider) + { + return s_themeProvider(m_images[m_current_type].key); + } + return QIcon::fromTheme(m_images[m_current_type].key); + } + + void IconEntry::remove(IconType rm_type) + { + m_images[rm_type].filename = QString(); + m_images[rm_type].icon = QIcon(); + for (auto iter = rm_type; iter != IconType::ToBeDeleted; iter--) + { + if (m_images[iter].present()) + { + m_current_type = iter; + return; + } + } + m_current_type = IconType::ToBeDeleted; + } + + void IconEntry::replace(IconType new_type, QIcon icon, QString path) + { + if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) + { + m_current_type = new_type; + } + m_images[new_type].icon = icon; + m_images[new_type].filename = path; + m_images[new_type].key = QString(); + } + + void IconEntry::replace(IconType new_type, const QString& key) + { + if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) + { + m_current_type = new_type; + } + m_images[new_type].icon = QIcon(); + m_images[new_type].filename = QString(); + m_images[new_type].key = key; + } + + QString IconEntry::getFilePath() const + { + if (m_current_type == IconType::ToBeDeleted) + { + return QString(); + } + return m_images[m_current_type].filename; + } + + bool IconEntry::isBuiltIn() const + { + return m_current_type == IconType::Builtin; + } +} // namespace projt::icons diff --git a/archived/projt-launcher/launcher/icons/IconEntry.hpp b/archived/projt-launcher/launcher/icons/IconEntry.hpp new file mode 100644 index 0000000000..f38fc318ea --- /dev/null +++ b/archived/projt-launcher/launcher/icons/IconEntry.hpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include + +namespace projt::icons +{ + enum IconType : unsigned + { + Builtin, + Transient, + FileBased, + ICONS_TOTAL, + ToBeDeleted + }; + + struct IconImage + { + QIcon icon; + QString key; + QString filename; + bool present() const + { + return !icon.isNull() || !key.isEmpty(); + } + }; + + struct IconEntry + { + QString m_key; + QString m_name; + IconImage m_images[ICONS_TOTAL]; + IconType m_current_type = ToBeDeleted; + + IconType type() const; + QString name() const; + bool has(IconType _type) const; + QIcon icon() const; + void remove(IconType rm_type); + void replace(IconType new_type, QIcon icon, QString path = QString()); + void replace(IconType new_type, const QString& key); + bool isBuiltIn() const; + QString getFilePath() const; + + using ThemeIconProvider = std::function; + static void setThemeIconProvider(ThemeIconProvider provider); + + private: + static ThemeIconProvider s_themeProvider; + }; +} // namespace projt::icons diff --git a/archived/projt-launcher/launcher/icons/IconList.cpp b/archived/projt-launcher/launcher/icons/IconList.cpp new file mode 100644 index 0000000000..13181117fa --- /dev/null +++ b/archived/projt-launcher/launcher/icons/IconList.cpp @@ -0,0 +1,552 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "IconList.hpp" +#include +#include +#include +#include +#include +#include +#include +#include "icons/IconUtils.hpp" +#include + +#define MAX_SIZE 1024 + +namespace projt::icons +{ + IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) + : QAbstractListModel(parent) + { + QSet builtinNames; + + // add builtin icons + for (const auto& builtinPath : builtinPaths) + { + QDir instanceIcons(builtinPath); + auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name); + for (const auto& fileInfo : fileInfoList) + { + builtinNames.insert(fileInfo.completeBaseName()); + } + } + for (const auto& builtinName : builtinNames) + { + addThemeIcon(builtinName); + } + + m_watcher.reset(new QFileSystemWatcher()); + connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged); + connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged); + + directoryChanged(path); + + // Forces the UI to update, so that lengthy icon names are shown properly from the start + emit iconUpdated({}); + } + + void IconList::sortIconList() + { + qDebug() << "Sorting icon list..."; + std::sort(m_icons.begin(), + m_icons.end(), + [](const IconEntry& a, const IconEntry& b) + { + bool aIsSubdir = a.m_key.contains(QDir::separator()); + bool bIsSubdir = b.m_key.contains(QDir::separator()); + if (aIsSubdir != bIsSubdir) + { + return !aIsSubdir; // root-level icons come first + } + return a.m_key.localeAwareCompare(b.m_key) < 0; + }); + reindex(); + } + + // Helper function to add directories recursively + bool IconList::addPathRecursively(const QString& path) + { + QDir dir(path); + if (!dir.exists()) + { + return false; + } + + // Add the directory itself + bool watching = m_watcher->addPath(path); + + // Add all subdirectories + QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo& entry : entries) + { + if (addPathRecursively(entry.absoluteFilePath())) + { + watching = true; + } + } + return watching; + } + + QStringList IconList::getIconFilePaths() const + { + QStringList iconFiles{}; + QStringList directories{ m_dir.absolutePath() }; + while (!directories.isEmpty()) + { + QString first = directories.takeFirst(); + QDir dir(first); + for (QFileInfo& fileInfo : + dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) + { + if (fileInfo.isDir()) + { + directories.push_back(fileInfo.absoluteFilePath()); + } + else + { + iconFiles.push_back(fileInfo.absoluteFilePath()); + } + } + } + return iconFiles; + } + + namespace + { + QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) + { + if (iconFile.dir() == iconsDir) + { + return iconFile.completeBaseName(); + } + + constexpr auto delimiter = " » "; + QString relativePathWithoutExtension = + iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.completeBaseName(); + return relativePathWithoutExtension.replace(QDir::separator(), delimiter); + } + + /// Split into a separate function because the preprocessing impedes readability + QSet toStringSet(const QList& list) + { + QSet set(list.begin(), list.end()); + return set; + } + } // namespace + + void IconList::directoryChanged(const QString& path) + { + QDir newDir(path); + if (m_dir.absolutePath() != newDir.absolutePath()) + { + if (!path.startsWith(m_dir.absolutePath())) + { + m_dir.setPath(path); + } + m_dir.refresh(); + if (m_isWatching) + { + stopWatching(); + } + startWatching(); + } + m_dir.refresh(); + if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath())) + { + return; + } + m_dir.refresh(); + const QStringList newFileNamesList = getIconFilePaths(); + const QSet newSet = toStringSet(newFileNamesList); + QSet currentSet; + for (const IconEntry& it : m_icons) + { + if (!it.has(IconType::FileBased)) + { + continue; + } + QFileInfo icon(it.getFilePath()); + currentSet.insert(icon.absoluteFilePath()); + } + QSet toRemove = currentSet - newSet; + QSet toAdd = newSet - currentSet; + + for (const QString& removedPath : toRemove) + { + qDebug() << "Removing icon " << removedPath; + QFileInfo removedFile(removedPath); + QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); + QString key = QFileInfo(relativePath).completeBaseName(); + + int idx = getIconIndex(key); + if (idx == -1) + { + continue; + } + m_icons[idx].remove(FileBased); + if (m_icons[idx].type() == ToBeDeleted) + { + beginRemoveRows(QModelIndex(), idx, idx); + m_icons.remove(idx); + reindex(); + endRemoveRows(); + } + else + { + dataChanged(index(idx), index(idx)); + } + m_watcher->removePath(removedPath); + emit iconUpdated(key); + } + + for (const QString& addedPath : toAdd) + { + qDebug() << "Adding icon " << addedPath; + + QFileInfo addfile(addedPath); + QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath()); + QString key = QFileInfo(relativePath).completeBaseName(); + QString name = formatName(m_dir, addfile); + + if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) + { + m_watcher->addPath(addedPath); + emit iconUpdated(key); + } + } + + sortIconList(); + } + + void IconList::fileChanged(const QString& path) + { + qDebug() << "Checking icon " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + { + return; + } + QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath()); + int idx = getIconIndex(key); + if (idx == -1) + { + return; + } + QIcon icon(path); + if (icon.availableSizes().empty()) + { + return; + } + + m_icons[idx].m_images[IconType::FileBased].icon = icon; + dataChanged(index(idx), index(idx)); + emit iconUpdated(key); + } + + void IconList::SettingChanged(const Setting& setting, const QVariant& value) + { + if (setting.id() != "IconsDir") + { + return; + } + + directoryChanged(value.toString()); + } + + void IconList::startWatching() + { + auto abs_path = m_dir.absolutePath(); + FS::ensureFolderPathExists(abs_path); + m_isWatching = addPathRecursively(abs_path); + if (m_isWatching) + { + qDebug() << "Started watching " << abs_path; + } + else + { + qDebug() << "Failed to start watching " << abs_path; + } + } + + void IconList::stopWatching() + { + m_watcher->removePaths(m_watcher->files()); + m_watcher->removePaths(m_watcher->directories()); + m_isWatching = false; + } + + QStringList IconList::mimeTypes() const + { + QStringList types; + types << "text/uri-list"; + return types; + } + + Qt::DropActions IconList::supportedDropActions() const + { + return Qt::CopyAction; + } + + bool IconList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) + { + if (action == Qt::IgnoreAction) + { + return true; + } + // check if the action is supported + if (!data || !(action & supportedDropActions())) + { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + QStringList iconFiles; + for (const auto& url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + { + continue; + } + iconFiles += url.toLocalFile(); + } + installIcons(iconFiles); + return true; + } + return false; + } + + Qt::ItemFlags IconList::flags(const QModelIndex& index) const + { + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + return Qt::ItemIsDropEnabled | defaultFlags; + } + + QVariant IconList::data(const QModelIndex& index, int role) const + { + if (!index.isValid()) + { + return {}; + } + + int row = index.row(); + + if (row < 0 || row >= m_icons.size()) + { + return {}; + } + + switch (role) + { + case Qt::DecorationRole: return m_icons[row].icon(); + case Qt::DisplayRole: return m_icons[row].name(); + case Qt::UserRole: return m_icons[row].m_key; + default: return {}; + } + } + + int IconList::rowCount(const QModelIndex& parent) const + { + return parent.isValid() ? 0 : m_icons.size(); + } + + void IconList::installIcons(const QStringList& iconFiles) + { + for (const QString& file : iconFiles) + { + installIcon(file, {}); + } + } + + void IconList::installIcon(const QString& file, const QString& name) + { + QFileInfo fileinfo(file); + if (!fileinfo.isReadable() || !fileinfo.isFile()) + { + return; + } + + if (!isIconSuffix(fileinfo.suffix())) + { + return; + } + + QString target = FS::PathCombine(getDirectory(), name.isEmpty() ? fileinfo.fileName() : name); + QFile::copy(file, target); + } + + bool IconList::iconFileExists(const QString& key) const + { + auto iconEntry = icon(key); + return iconEntry && iconEntry->has(IconType::FileBased); + } + + /// Returns the icon with the given key or nullptr if it doesn't exist. + const IconEntry* IconList::icon(const QString& key) const + { + int iconIdx = getIconIndex(key); + if (iconIdx == -1) + { + return nullptr; + } + return &m_icons[iconIdx]; + } + + bool IconList::deleteIcon(const QString& key) + { + auto* iconEntry = icon(key); + return iconEntry && iconFileExists(key) && FS::deletePath(iconEntry->getFilePath()); + } + + bool IconList::trashIcon(const QString& key) + { + auto* iconEntry = icon(key); + return iconEntry && iconFileExists(key) && FS::trash(iconEntry->getFilePath(), nullptr); + } + + bool IconList::addThemeIcon(const QString& key) + { + auto iter = m_nameIndex.find(key); + if (iter != m_nameIndex.end()) + { + auto& oldOne = m_icons[*iter]; + oldOne.replace(Builtin, key); + dataChanged(index(*iter), index(*iter)); + return true; + } + // add a new icon + beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); + { + IconEntry iconEntry; + iconEntry.m_name = key; + iconEntry.m_key = key; + iconEntry.replace(Builtin, key); + m_icons.push_back(iconEntry); + m_nameIndex[key] = m_icons.size() - 1; + } + endInsertRows(); + return true; + } + + bool IconList::addIcon(const QString& key, const QString& name, const QString& path, const IconType type) + { + // replace the icon even? is the input valid? + QIcon icon(path); + if (icon.isNull()) + { + return false; + } + auto iter = m_nameIndex.find(key); + if (iter != m_nameIndex.end()) + { + auto& oldOne = m_icons[*iter]; + oldOne.replace(type, icon, path); + dataChanged(index(*iter), index(*iter)); + return true; + } + // add a new icon + beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); + { + IconEntry iconEntry; + iconEntry.m_name = name; + iconEntry.m_key = key; + iconEntry.replace(type, icon, path); + m_icons.push_back(iconEntry); + m_nameIndex[key] = m_icons.size() - 1; + } + endInsertRows(); + return true; + } + + void IconList::saveIcon(const QString& key, const QString& path, const char* format) const + { + auto icon = getIcon(key); + auto pixmap = icon.pixmap(128, 128); + pixmap.save(path, format); + } + + void IconList::reindex() + { + m_nameIndex.clear(); + for (int i = 0; i < m_icons.size(); i++) + { + m_nameIndex[m_icons[i].m_key] = i; + emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model + } + } + + QIcon IconList::getIcon(const QString& key) const + { + int iconIndex = getIconIndex(key); + + if (iconIndex != -1) + { + return m_icons[iconIndex].icon(); + } + + // Fallback for icons that don't exist. + iconIndex = getIconIndex("grass"); + + if (iconIndex != -1) + { + return m_icons[iconIndex].icon(); + } + return {}; + } + + int IconList::getIconIndex(const QString& key) const + { + auto iter = m_nameIndex.find(key == "default" ? "grass" : key); + if (iter != m_nameIndex.end()) + { + return *iter; + } + + return -1; + } + + QString IconList::getDirectory() const + { + return m_dir.absolutePath(); + } + + /// Returns the directory of the icon with the given key or the default directory if it's a builtin icon. + QString IconList::iconDirectory(const QString& key) const + { + for (const auto& iconEntry : m_icons) + { + if (iconEntry.m_key == key && iconEntry.has(IconType::FileBased)) + { + QFileInfo iconFile(iconEntry.getFilePath()); + return iconFile.dir().path(); + } + } + return getDirectory(); + } +} // namespace projt::icons diff --git a/archived/projt-launcher/launcher/icons/IconList.hpp b/archived/projt-launcher/launcher/icons/IconList.hpp new file mode 100644 index 0000000000..1cde1874d5 --- /dev/null +++ b/archived/projt-launcher/launcher/icons/IconList.hpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "IconEntry.hpp" +#include "settings/Setting.h" + +#include "QObjectPtr.h" + +class QFileSystemWatcher; +class QMimeData; + +namespace projt::icons +{ + class IconList : public QAbstractListModel + { + Q_OBJECT + public: + explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = nullptr); + ~IconList() override = default; + + QIcon getIcon(const QString& key) const; + int getIconIndex(const QString& key) const; + QString getDirectory() const; + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + QStringList mimeTypes() const override; + Qt::DropActions supportedDropActions() const override; + bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + + bool addThemeIcon(const QString& key); + bool addIcon(const QString& key, const QString& name, const QString& path, IconType type); + void saveIcon(const QString& key, const QString& path, const char* format) const; + bool deleteIcon(const QString& key); + bool trashIcon(const QString& key); + bool iconFileExists(const QString& key) const; + QString iconDirectory(const QString& key) const; + + void installIcons(const QStringList& iconFiles); + void installIcon(const QString& file, const QString& name); + + const IconEntry* icon(const QString& key) const; + + void startWatching(); + void stopWatching(); + + signals: + void iconUpdated(QString key); + + private: + IconList(const IconList&) = delete; + IconList& operator=(const IconList&) = delete; + + void reindex(); + void sortIconList(); + bool addPathRecursively(const QString& path); + QStringList getIconFilePaths() const; + + public slots: + void directoryChanged(const QString& path); + + protected slots: + void fileChanged(const QString& path); + void SettingChanged(const Setting& setting, const QVariant& value); + + private: + shared_qobject_ptr m_watcher; + bool m_isWatching = false; + QMap m_nameIndex; + QList m_icons; + QDir m_dir; + }; +} // namespace projt::icons diff --git a/archived/projt-launcher/launcher/icons/IconUtils.cpp b/archived/projt-launcher/launcher/icons/IconUtils.cpp new file mode 100644 index 0000000000..52343b68b7 --- /dev/null +++ b/archived/projt-launcher/launcher/icons/IconUtils.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "IconUtils.hpp" + +#include +#include "FileSystem.h" + +namespace +{ + static const QStringList validIconExtensions = { { "svg", "png", "ico", "gif", "jpg", "jpeg", "webp" } }; +} + +namespace projt::icons +{ + + QString findBestIconIn(const QString& folder, const QString& iconKey) + { + QString best_filename; + + QDirIterator it(folder, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::NoIteratorFlags); + while (it.hasNext()) + { + it.next(); + auto fileInfo = it.fileInfo(); + if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) + && isIconSuffix(fileInfo.suffix())) + return fileInfo.absoluteFilePath(); + } + return {}; + } + + QString getIconFilter() + { + return "(*." + validIconExtensions.join(" *.") + ")"; + } + + bool isIconSuffix(QString suffix) + { + return validIconExtensions.contains(suffix); + } + +} // namespace projt::icons diff --git a/archived/projt-launcher/launcher/icons/IconUtils.hpp b/archived/projt-launcher/launcher/icons/IconUtils.hpp new file mode 100644 index 0000000000..e0ed37b9dc --- /dev/null +++ b/archived/projt-launcher/launcher/icons/IconUtils.hpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +namespace projt::icons +{ + // Given a folder and an icon key, find 'best' of the icons with the given key in there and return its path + QString findBestIconIn(const QString& folder, const QString& iconKey); + + // Get icon file type filter for file browser dialogs + QString getIconFilter(); + + bool isIconSuffix(QString suffix); +} // namespace projt::icons diff --git a/archived/projt-launcher/launcher/java/core/RuntimeInstall.cpp b/archived/projt-launcher/launcher/java/core/RuntimeInstall.cpp new file mode 100644 index 0000000000..4eff57d0fe --- /dev/null +++ b/archived/projt-launcher/launcher/java/core/RuntimeInstall.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "java/core/RuntimeInstall.hpp" + +#include "BaseVersion.h" +#include "StringUtils.h" + +#include + +namespace projt::java +{ + bool RuntimeInstall::operator<(const RuntimeInstall& rhs) const + { + if (version < rhs.version) + { + return true; + } + if (version > rhs.version) + { + return false; + } + if (is_64bit != rhs.is_64bit) + { + return rhs.is_64bit; + } + auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); + if (archCompare != 0) + { + return archCompare < 0; + } + return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; + } + + bool RuntimeInstall::operator==(const RuntimeInstall& rhs) const + { + return version == rhs.version && arch == rhs.arch && path == rhs.path && is_64bit == rhs.is_64bit; + } + + bool RuntimeInstall::operator>(const RuntimeInstall& rhs) const + { + return (!operator<(rhs)) && (!operator==(rhs)); + } + + bool RuntimeInstall::operator<(BaseVersion& a) const + { + try + { + return operator<(dynamic_cast(a)); + } + catch (const std::bad_cast&) + { + return BaseVersion::operator<(a); + } + } + + bool RuntimeInstall::operator>(BaseVersion& a) const + { + try + { + return operator>(dynamic_cast(a)); + } + catch (const std::bad_cast&) + { + return BaseVersion::operator>(a); + } + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/core/RuntimeInstall.hpp b/archived/projt-launcher/launcher/java/core/RuntimeInstall.hpp new file mode 100644 index 0000000000..5039df2ecb --- /dev/null +++ b/archived/projt-launcher/launcher/java/core/RuntimeInstall.hpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "BaseVersion.h" +#include "java/core/RuntimeVersion.hpp" + +#include +#include + +namespace projt::java +{ + struct RuntimeInstall : public BaseVersion + { + RuntimeInstall() = default; + RuntimeInstall(RuntimeVersion version, QString arch, QString path) + : version(std::move(version)), + arch(std::move(arch)), + path(std::move(path)) + {} + + QString descriptor() const override + { + return version.toString(); + } + QString name() const override + { + return version.toString(); + } + QString typeString() const override + { + return arch; + } + + bool operator<(BaseVersion& a) const override; + bool operator>(BaseVersion& a) const override; + bool operator<(const RuntimeInstall& rhs) const; + bool operator==(const RuntimeInstall& rhs) const; + bool operator>(const RuntimeInstall& rhs) const; + + RuntimeVersion version; + QString arch; + QString path; + QString vendor; + bool recommended = false; + bool is_64bit = false; + bool managed = false; + }; + + using RuntimeInstallPtr = std::shared_ptr; +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/core/RuntimePackage.cpp b/archived/projt-launcher/launcher/java/core/RuntimePackage.cpp new file mode 100644 index 0000000000..727201d1bd --- /dev/null +++ b/archived/projt-launcher/launcher/java/core/RuntimePackage.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "java/core/RuntimePackage.hpp" + +#include "Json.h" +#include "StringUtils.h" +#include "minecraft/ParseUtils.h" + +#include + +namespace projt::java +{ + PackageKind parsePackageKind(const QString& raw) + { + if (raw == "manifest") + { + return PackageKind::Manifest; + } + if (raw == "archive") + { + return PackageKind::Archive; + } + return PackageKind::Unknown; + } + + QString packageKindToString(PackageKind kind) + { + switch (kind) + { + case PackageKind::Manifest: return "manifest"; + case PackageKind::Archive: return "archive"; + case PackageKind::Unknown: break; + } + return "unknown"; + } + + RuntimePackagePtr parseRuntimePackage(const QJsonObject& in) + { + auto meta = std::make_shared(); + + meta->displayName = Json::ensureString(in, "name", ""); + meta->vendor = Json::ensureString(in, "vendor", ""); + meta->url = Json::ensureString(in, "url", ""); + meta->releaseTime = timeFromS3Time(Json::ensureString(in, "releaseTime", "")); + meta->downloadKind = parsePackageKind(Json::ensureString(in, "downloadType", "")); + meta->packageType = Json::ensureString(in, "packageType", ""); + meta->runtimeOS = Json::ensureString(in, "runtimeOS", "unknown"); + + if (in.contains("checksum")) + { + auto obj = Json::requireObject(in, "checksum"); + meta->checksumHash = Json::ensureString(obj, "hash", ""); + meta->checksumType = Json::ensureString(obj, "type", ""); + } + + if (in.contains("version")) + { + auto obj = Json::requireObject(in, "version"); + auto name = Json::ensureString(obj, "name", ""); + auto major = Json::ensureInteger(obj, "major", 0); + auto minor = Json::ensureInteger(obj, "minor", 0); + auto security = Json::ensureInteger(obj, "security", 0); + auto build = Json::ensureInteger(obj, "build", 0); + meta->version = RuntimeVersion(major, minor, security, build, name); + } + return meta; + } + + bool RuntimePackage::operator<(const RuntimePackage& rhs) const + { + if (version < rhs.version) + { + return true; + } + if (version > rhs.version) + { + return false; + } + if (releaseTime < rhs.releaseTime) + { + return true; + } + if (releaseTime > rhs.releaseTime) + { + return false; + } + return StringUtils::naturalCompare(displayName, rhs.displayName, Qt::CaseInsensitive) < 0; + } + + bool RuntimePackage::operator==(const RuntimePackage& rhs) const + { + return version == rhs.version && displayName == rhs.displayName; + } + + bool RuntimePackage::operator>(const RuntimePackage& rhs) const + { + return (!operator<(rhs)) && (!operator==(rhs)); + } + + bool RuntimePackage::operator<(BaseVersion& a) const + { + try + { + return operator<(dynamic_cast(a)); + } + catch (const std::bad_cast&) + { + return BaseVersion::operator<(a); + } + } + + bool RuntimePackage::operator>(BaseVersion& a) const + { + try + { + return operator>(dynamic_cast(a)); + } + catch (const std::bad_cast&) + { + return BaseVersion::operator>(a); + } + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/core/RuntimePackage.hpp b/archived/projt-launcher/launcher/java/core/RuntimePackage.hpp new file mode 100644 index 0000000000..88ebaadf2c --- /dev/null +++ b/archived/projt-launcher/launcher/java/core/RuntimePackage.hpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include + +#include + +#include "BaseVersion.h" +#include "java/core/RuntimeVersion.hpp" + +namespace projt::java +{ + enum class PackageKind + { + Manifest, + Archive, + Unknown + }; + + class RuntimePackage : public BaseVersion + { + public: + QString descriptor() const override + { + return version.toString(); + } + + QString name() const override + { + return displayName; + } + + QString typeString() const override + { + return vendor; + } + + bool operator<(BaseVersion& a) const override; + bool operator>(BaseVersion& a) const override; + bool operator<(const RuntimePackage& rhs) const; + bool operator==(const RuntimePackage& rhs) const; + bool operator>(const RuntimePackage& rhs) const; + + QString displayName; + QString vendor; + QString url; + QDateTime releaseTime; + QString checksumType; + QString checksumHash; + PackageKind downloadKind = PackageKind::Unknown; + QString packageType; + RuntimeVersion version; + QString runtimeOS; + }; + + using RuntimePackagePtr = std::shared_ptr; + + PackageKind parsePackageKind(const QString& raw); + QString packageKindToString(PackageKind kind); + RuntimePackagePtr parseRuntimePackage(const QJsonObject& obj); +} // namespace projt::java \ No newline at end of file diff --git a/archived/projt-launcher/launcher/java/core/RuntimeVersion.cpp b/archived/projt-launcher/launcher/java/core/RuntimeVersion.cpp new file mode 100644 index 0000000000..cd2e07a9d9 --- /dev/null +++ b/archived/projt-launcher/launcher/java/core/RuntimeVersion.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "java/core/RuntimeVersion.hpp" + +#include "StringUtils.h" + +#include + +namespace projt::java +{ + RuntimeVersion::RuntimeVersion(const QString& raw) + { + parse(raw); + } + + RuntimeVersion::RuntimeVersion(int major, int minor, int security, int build, QString name) + : m_major(major), + m_minor(minor), + m_security(security), + m_name(name), + m_valid(true) + { + QStringList parts; + if (build != 0) + { + m_prerelease = QString::number(build); + parts.push_front(m_prerelease); + } + if (m_security != 0) + { + parts.push_front(QString::number(m_security)); + } + else if (!parts.isEmpty()) + { + parts.push_front("0"); + } + + if (m_minor != 0) + { + parts.push_front(QString::number(m_minor)); + } + else if (!parts.isEmpty()) + { + parts.push_front("0"); + } + parts.push_front(QString::number(m_major)); + m_raw = parts.join("."); + } + + RuntimeVersion& RuntimeVersion::operator=(const QString& raw) + { + parse(raw); + return *this; + } + + void RuntimeVersion::parse(const QString& raw) + { + m_raw = raw.trimmed(); + m_major = 0; + m_minor = 0; + m_security = 0; + m_prerelease.clear(); + m_valid = false; + + QString numeric = m_raw; + auto dashIndex = numeric.indexOf('-'); + if (dashIndex >= 0) + { + m_prerelease = numeric.mid(dashIndex + 1).trimmed(); + numeric = numeric.left(dashIndex); + } + + int updateNumber = 0; + auto updateIndex = numeric.indexOf('_'); + if (updateIndex >= 0) + { + bool ok = false; + updateNumber = numeric.mid(updateIndex + 1).toInt(&ok); + if (!ok) + { + updateNumber = 0; + } + numeric = numeric.left(updateIndex); + } + + numeric.replace('_', '.'); + QStringList parts = numeric.split('.', Qt::SkipEmptyParts); + if (!parts.isEmpty() && parts[0] == "1" && parts.size() > 1) + { + parts.removeFirst(); + } + if (parts.isEmpty()) + { + return; + } + + auto parsePart = [](const QString& value, int& out) + { + bool ok = false; + int parsed = value.toInt(&ok); + if (!ok) + { + return false; + } + out = parsed; + return true; + }; + + if (!parsePart(parts[0], m_major)) + { + return; + } + m_valid = true; + if (parts.size() > 1) + { + parsePart(parts[1], m_minor); + } + if (parts.size() > 2) + { + parsePart(parts[2], m_security); + } + if (updateNumber != 0) + { + m_security = updateNumber; + } + } + + QString RuntimeVersion::toString() const + { + return m_raw; + } + + bool RuntimeVersion::needsPermGen() const + { + return !m_valid || m_major <= 7; + } + + bool RuntimeVersion::defaultsToUtf8() const + { + return m_valid && m_major >= 18; + } + + bool RuntimeVersion::supportsModules() const + { + return m_valid && m_major >= 9; + } + + bool RuntimeVersion::operator<(const RuntimeVersion& rhs) const + { + if (m_valid && rhs.m_valid) + { + if (m_major != rhs.m_major) + { + return m_major < rhs.m_major; + } + if (m_minor != rhs.m_minor) + { + return m_minor < rhs.m_minor; + } + if (m_security != rhs.m_security) + { + return m_security < rhs.m_security; + } + + bool thisPre = !m_prerelease.isEmpty(); + bool rhsPre = !rhs.m_prerelease.isEmpty(); + if (thisPre != rhsPre) + { + return thisPre; + } + if (thisPre && rhsPre) + { + return StringUtils::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0; + } + return false; + } + return StringUtils::naturalCompare(m_raw, rhs.m_raw, Qt::CaseSensitive) < 0; + } + + bool RuntimeVersion::operator==(const RuntimeVersion& rhs) const + { + if (m_valid && rhs.m_valid) + { + return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security + && m_prerelease == rhs.m_prerelease; + } + return m_raw == rhs.m_raw; + } + + bool RuntimeVersion::operator>(const RuntimeVersion& rhs) const + { + return (!operator<(rhs)) && (!operator==(rhs)); + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/core/RuntimeVersion.hpp b/archived/projt-launcher/launcher/java/core/RuntimeVersion.hpp new file mode 100644 index 0000000000..7199add778 --- /dev/null +++ b/archived/projt-launcher/launcher/java/core/RuntimeVersion.hpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +#ifdef major +#undef major +#endif +#ifdef minor +#undef minor +#endif + +namespace projt::java +{ + class RuntimeVersion + { + friend class RuntimeVersionTest; + + public: + RuntimeVersion() = default; + explicit RuntimeVersion(const QString& raw); + RuntimeVersion(int major, int minor, int security, int build = 0, QString name = ""); + + RuntimeVersion& operator=(const QString& raw); + + bool operator<(const RuntimeVersion& rhs) const; + bool operator==(const RuntimeVersion& rhs) const; + bool operator>(const RuntimeVersion& rhs) const; + + bool needsPermGen() const; + bool defaultsToUtf8() const; + bool supportsModules() const; + + QString toString() const; + + int major() const + { + return m_major; + } + int minor() const + { + return m_minor; + } + int security() const + { + return m_security; + } + QString prerelease() const + { + return m_prerelease; + } + QString name() const + { + return m_name; + } + + private: + void parse(const QString& raw); + + QString m_raw; + int m_major = 0; + int m_minor = 0; + int m_security = 0; + QString m_name; + bool m_valid = false; + QString m_prerelease; + }; +} // namespace projt::java \ No newline at end of file diff --git a/archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.cpp b/archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.cpp new file mode 100644 index 0000000000..9ab74afe62 --- /dev/null +++ b/archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.cpp @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "java/download/RuntimeArchiveTask.hpp" + +#include +#include +#include +#include + +#include "Application.h" +#include "MMCZip.h" +#include "Untar.h" +#include "net/ChecksumValidator.h" +#include "net/NetJob.h" + +namespace projt::java +{ + RuntimeArchiveTask::RuntimeArchiveTask(QUrl url, QString finalPath, QString checksumType, QString checksumHash) + : m_url(url), + m_final_path(std::move(finalPath)), + m_checksum_type(std::move(checksumType)), + m_checksum_hash(std::move(checksumHash)) + {} + + void RuntimeArchiveTask::executeTask() + { + setStatus(tr("Downloading Java")); + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("java", m_url.fileName()); + + auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); + auto action = Net::Download::makeCached(m_url, entry); + if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) + { + auto hashType = QCryptographicHash::Algorithm::Sha1; + if (m_checksum_type == "sha256") + { + hashType = QCryptographicHash::Algorithm::Sha256; + } + action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); + } + download->addNetAction(action); + auto fullPath = entry->getFullPath(); + + connect(download.get(), &Task::failed, this, &RuntimeArchiveTask::emitFailed); + connect(download.get(), &Task::progress, this, &RuntimeArchiveTask::setProgress); + connect(download.get(), &Task::stepProgress, this, &RuntimeArchiveTask::propagateStepProgress); + connect(download.get(), &Task::status, this, &RuntimeArchiveTask::setStatus); + connect(download.get(), &Task::details, this, &RuntimeArchiveTask::setDetails); + connect(download.get(), &Task::aborted, this, &RuntimeArchiveTask::emitAborted); + connect(download.get(), &Task::succeeded, this, [this, fullPath] { extractRuntime(fullPath); }); + m_task = download; + m_task->start(); + } + + void RuntimeArchiveTask::extractRuntime(QString input) + { + setStatus(tr("Extracting Java")); + if (input.endsWith("tar")) + { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + QFile in(input); + if (!in.open(QFile::ReadOnly)) + { + emitFailed(tr("Unable to open supplied tar file.")); + return; + } + if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) + { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } + if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) + { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) + { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } + if (input.endsWith("zip")) + { + auto zip = std::make_shared(input); + if (!zip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } + auto files = zip->getFileNameList(); + if (files.isEmpty()) + { + emitFailed(tr("No files were found in the supplied zip file.")); + return; + } + m_task = makeShared(zip, m_final_path, files[0]); + + auto progressStep = std::make_shared(); + connect(m_task.get(), + &Task::finished, + this, + [this, progressStep] + { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(m_task.get(), &Task::succeeded, this, &RuntimeArchiveTask::emitSucceeded); + connect(m_task.get(), &Task::aborted, this, &RuntimeArchiveTask::emitAborted); + connect(m_task.get(), + &Task::failed, + this, + [this, progressStep](QString reason) + { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &RuntimeArchiveTask::propagateStepProgress); + + connect(m_task.get(), + &Task::progress, + this, + [this, progressStep](qint64 current, qint64 total) + { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(m_task.get(), + &Task::status, + this, + [this, progressStep](QString status) + { + progressStep->status = status; + stepProgress(*progressStep); + }); + m_task->start(); + return; + } + + emitFailed(tr("Could not determine archive type!")); + } + + bool RuntimeArchiveTask::abort() + { + auto aborted = canAbort(); + if (m_task) + aborted = m_task->abort(); + return aborted; + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.hpp b/archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.hpp new file mode 100644 index 0000000000..6aa524ceb2 --- /dev/null +++ b/archived/projt-launcher/launcher/java/download/RuntimeArchiveTask.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +#include "tasks/Task.h" + +namespace projt::java +{ + class RuntimeArchiveTask : public Task + { + Q_OBJECT + public: + RuntimeArchiveTask(QUrl url, QString finalPath, QString checksumType = "", QString checksumHash = ""); + ~RuntimeArchiveTask() override = default; + + bool canAbort() const override + { + return true; + } + + bool abort() override; + + protected: + void executeTask() override; + + private: + void extractRuntime(QString input); + + QUrl m_url; + QString m_final_path; + QString m_checksum_type; + QString m_checksum_hash; + Task::Ptr m_task; + }; +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/download/RuntimeLinkTask.cpp b/archived/projt-launcher/launcher/java/download/RuntimeLinkTask.cpp new file mode 100644 index 0000000000..fd5b381044 --- /dev/null +++ b/archived/projt-launcher/launcher/java/download/RuntimeLinkTask.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "java/download/RuntimeLinkTask.hpp" + +#include +#include + +#include "FileSystem.h" + +namespace +{ + QString findBinPath(const QString& root, const QString& pattern) + { + auto path = FS::PathCombine(root, pattern); + if (QFileInfo::exists(path)) + { + return path; + } + + auto entries = QDir(root).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (auto& entry : entries) + { + path = FS::PathCombine(entry.absoluteFilePath(), pattern); + if (QFileInfo::exists(path)) + { + return path; + } + } + + return {}; + } +} // namespace + +namespace projt::java +{ + RuntimeLinkTask::RuntimeLinkTask(QString finalPath) : m_path(std::move(finalPath)) + {} + + void RuntimeLinkTask::executeTask() + { + setStatus(tr("Checking for Java binary path")); + const auto binPath = FS::PathCombine("bin", "java"); + const auto wantedPath = FS::PathCombine(m_path, binPath); + if (QFileInfo::exists(wantedPath)) + { + emitSucceeded(); + return; + } + + setStatus(tr("Searching for Java binary path")); + const auto contentsPartialPath = FS::PathCombine("Contents", "Home", binPath); + const auto relativePathToBin = findBinPath(m_path, contentsPartialPath); + if (relativePathToBin.isEmpty()) + { + emitFailed(tr("Failed to find Java binary path")); + return; + } + const auto folderToLink = relativePathToBin.chopped(binPath.length()); + + setStatus(tr("Collecting folders to symlink")); + auto entries = QDir(folderToLink).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries); + QList files; + setProgress(0, entries.length()); + for (auto& entry : entries) + { + files.append({ entry.absoluteFilePath(), FS::PathCombine(m_path, entry.fileName()) }); + } + + setStatus(tr("Symlinking Java binary path")); + FS::create_link folderLink(files); + connect(&folderLink, + &FS::create_link::fileLinked, + [this](QString, QString) { setProgress(m_progress + 1, m_progressTotal); }); + if (!folderLink()) + { + emitFailed(folderLink.getOSError().message().c_str()); + } + else + { + emitSucceeded(); + } + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/download/RuntimeLinkTask.hpp b/archived/projt-launcher/launcher/java/download/RuntimeLinkTask.hpp new file mode 100644 index 0000000000..d4daf98a04 --- /dev/null +++ b/archived/projt-launcher/launcher/java/download/RuntimeLinkTask.hpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "tasks/Task.h" + +namespace projt::java +{ + class RuntimeLinkTask : public Task + { + Q_OBJECT + public: + explicit RuntimeLinkTask(QString finalPath); + ~RuntimeLinkTask() override = default; + + protected: + void executeTask() override; + + private: + QString m_path; + }; +} // namespace projt::java \ No newline at end of file diff --git a/archived/projt-launcher/launcher/java/download/RuntimeManifestTask.cpp b/archived/projt-launcher/launcher/java/download/RuntimeManifestTask.cpp new file mode 100644 index 0000000000..3776078ecf --- /dev/null +++ b/archived/projt-launcher/launcher/java/download/RuntimeManifestTask.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "java/download/RuntimeManifestTask.hpp" + +#include +#include + +#include "Application.h" +#include "FileSystem.h" +#include "Json.h" +#include "net/ChecksumValidator.h" +#include "net/NetJob.h" + +namespace +{ + struct FileEntry + { + QString path; + QString url; + QByteArray hash; + bool isExec = false; + }; +} // namespace + +namespace projt::java +{ + RuntimeManifestTask::RuntimeManifestTask(QUrl url, QString finalPath, QString checksumType, QString checksumHash) + : m_url(url), + m_final_path(std::move(finalPath)), + m_checksum_type(std::move(checksumType)), + m_checksum_hash(std::move(checksumHash)) + {} + + void RuntimeManifestTask::executeTask() + { + setStatus(tr("Downloading Java")); + auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); + auto files = std::make_shared(); + + auto action = Net::Download::makeByteArray(m_url, files); + if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) + { + auto hashType = QCryptographicHash::Algorithm::Sha1; + if (m_checksum_type == "sha256") + { + hashType = QCryptographicHash::Algorithm::Sha256; + } + action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); + } + download->addNetAction(action); + + connect(download.get(), &Task::failed, this, &RuntimeManifestTask::emitFailed); + connect(download.get(), &Task::progress, this, &RuntimeManifestTask::setProgress); + connect(download.get(), &Task::stepProgress, this, &RuntimeManifestTask::propagateStepProgress); + connect(download.get(), &Task::status, this, &RuntimeManifestTask::setStatus); + connect(download.get(), &Task::details, this, &RuntimeManifestTask::setDetails); + + connect(download.get(), + &Task::succeeded, + [files, this] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*files, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response at " << parse_error.offset + << ". Reason: " << parse_error.errorString(); + qWarning() << *files; + emitFailed(parse_error.errorString()); + return; + } + downloadRuntime(doc); + }); + m_task = download; + m_task->start(); + } + + void RuntimeManifestTask::downloadRuntime(const QJsonDocument& doc) + { + FS::ensureFolderPathExists(m_final_path); + std::vector toDownload; + auto list = Json::ensureObject(Json::ensureObject(doc.object()), "files"); + for (const auto& pathKey : list.keys()) + { + auto filePath = FS::PathCombine(m_final_path, pathKey); + const QJsonObject& meta = Json::ensureObject(list, pathKey); + auto type = Json::ensureString(meta, "type"); + if (type == "directory") + { + FS::ensureFolderPathExists(filePath); + } + else if (type == "link") + { + auto target = Json::ensureString(meta, "target"); + if (!target.isEmpty()) + { + QFile::link(target, filePath); + } + } + else if (type == "file") + { + auto downloads = Json::ensureObject(meta, "downloads"); + auto isExec = Json::ensureBoolean(meta, "executable", false); + QString url; + QByteArray hash; + + if (downloads.contains("raw")) + { + auto raw = Json::ensureObject(downloads, "raw"); + url = Json::ensureString(raw, "url"); + hash = QByteArray::fromHex(Json::ensureString(raw, "sha1").toLatin1()); + } + else + { + qWarning() << "No raw download available for file:" << pathKey; + qWarning() << "Skipping file without raw download - decompression not yet supported"; + continue; + } + + if (!url.isEmpty() && QUrl(url).isValid()) + { + toDownload.push_back({ filePath, url, hash, isExec }); + } + } + } + auto elementDownload = makeShared("JRE::FileDownload", APPLICATION->network()); + for (const auto& file : toDownload) + { + auto dl = Net::Download::makeFile(file.url, file.path); + if (!file.hash.isEmpty()) + { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, file.hash)); + } + if (file.isExec) + { + connect(dl.get(), + &Net::Download::succeeded, + [file] + { + QFile(file.path).setPermissions(QFile(file.path).permissions() + | QFileDevice::Permissions(0x1111)); + }); + } + elementDownload->addNetAction(dl); + } + + connect(elementDownload.get(), &Task::failed, this, &RuntimeManifestTask::emitFailed); + connect(elementDownload.get(), &Task::progress, this, &RuntimeManifestTask::setProgress); + connect(elementDownload.get(), &Task::stepProgress, this, &RuntimeManifestTask::propagateStepProgress); + connect(elementDownload.get(), &Task::status, this, &RuntimeManifestTask::setStatus); + connect(elementDownload.get(), &Task::details, this, &RuntimeManifestTask::setDetails); + + connect(elementDownload.get(), &Task::succeeded, this, &RuntimeManifestTask::emitSucceeded); + m_task = elementDownload; + m_task->start(); + } + + bool RuntimeManifestTask::abort() + { + auto aborted = canAbort(); + if (m_task) + aborted = m_task->abort(); + emitAborted(); + return aborted; + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/download/RuntimeManifestTask.hpp b/archived/projt-launcher/launcher/java/download/RuntimeManifestTask.hpp new file mode 100644 index 0000000000..6eb8427dbe --- /dev/null +++ b/archived/projt-launcher/launcher/java/download/RuntimeManifestTask.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +#include "tasks/Task.h" + +namespace projt::java +{ + class RuntimeManifestTask : public Task + { + Q_OBJECT + public: + RuntimeManifestTask(QUrl url, QString finalPath, QString checksumType = "", QString checksumHash = ""); + ~RuntimeManifestTask() override = default; + + bool canAbort() const override + { + return true; + } + + bool abort() override; + + protected: + void executeTask() override; + + private: + void downloadRuntime(const QJsonDocument& doc); + + QUrl m_url; + QString m_final_path; + QString m_checksum_type; + QString m_checksum_hash; + Task::Ptr m_task; + }; +} // namespace projt::java \ No newline at end of file diff --git a/archived/projt-launcher/launcher/java/services/RuntimeCatalog.cpp b/archived/projt-launcher/launcher/java/services/RuntimeCatalog.cpp new file mode 100644 index 0000000000..f7fbc00602 --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeCatalog.cpp @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "java/services/RuntimeCatalog.hpp" + +#include +#include + +#include +#include + +#include "Application.h" +#include "java/services/RuntimeScanner.hpp" +#include "tasks/ConcurrentTask.h" + +namespace projt::java +{ + RuntimeCatalog::RuntimeCatalog(QObject* parent, Scope scope) : BaseVersionList(parent), m_scope(scope) + {} + + Task::Ptr RuntimeCatalog::getLoadTask() + { + load(); + return currentTask(); + } + + Task::Ptr RuntimeCatalog::currentTask() const + { + if (m_state == State::Loading) + { + return m_task; + } + return nullptr; + } + + void RuntimeCatalog::load() + { + if (m_state != State::Loading) + { + m_state = State::Loading; + m_task.reset(new RuntimeCatalogTask(this, m_scope)); + m_task->start(); + } + } + + const BaseVersion::Ptr RuntimeCatalog::at(int i) const + { + return m_entries.at(i); + } + + bool RuntimeCatalog::isLoaded() + { + return m_state == State::Ready; + } + + int RuntimeCatalog::count() const + { + return m_entries.count(); + } + + QVariant RuntimeCatalog::data(const QModelIndex& index, int role) const + { + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto runtime = std::dynamic_pointer_cast(m_entries[index.row()]); + switch (role) + { + case SortRole: return -index.row(); + case VersionPointerRole: return QVariant::fromValue(m_entries[index.row()]); + case VersionIdRole: return runtime->descriptor(); + case VersionRole: return runtime->version.toString(); + case RecommendedRole: return false; + case PathRole: return runtime->path; + case CPUArchitectureRole: return runtime->arch; + default: return QVariant(); + } + } + + BaseVersionList::RoleList RuntimeCatalog::providesRoles() const + { + return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, CPUArchitectureRole }; + } + + void RuntimeCatalog::updateListData(QList versions) + { + beginResetModel(); + m_entries = std::move(versions); + sortVersions(); + endResetModel(); + m_state = State::Ready; + m_task.reset(); + } + + static bool sortRuntimes(BaseVersion::Ptr left, BaseVersion::Ptr right) + { + auto rleft = std::dynamic_pointer_cast(right); + auto rright = std::dynamic_pointer_cast(left); + return (*rleft) > (*rright); + } + + void RuntimeCatalog::sortVersions() + { + std::sort(m_entries.begin(), m_entries.end(), sortRuntimes); + } + + RuntimeCatalogTask::RuntimeCatalogTask(RuntimeCatalog* catalog, RuntimeCatalog::Scope scope) + : Task(), + m_catalog(catalog), + m_scope(scope) + {} + + void RuntimeCatalogTask::executeTask() + { + setStatus(tr("Detecting Java installations...")); + + RuntimeScanner scanner; + QStringList candidatePaths = scanner.collectPaths(m_scope == RuntimeCatalog::Scope::ManagedOnly); + + ConcurrentTask::Ptr job( + new ConcurrentTask("Runtime detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + m_job.reset(job); + connect(m_job.get(), &Task::finished, this, &RuntimeCatalogTask::probeFinished); + connect(m_job.get(), &Task::progress, this, &Task::setProgress); + + qDebug() << "Probing the following runtime paths:"; + int token = 0; + for (const auto& candidate : candidatePaths) + { + RuntimeProbeTask::ProbeSettings settings; + settings.binaryPath = candidate; + settings.token = token; + auto probe = new RuntimeProbeTask(settings); + connect(probe, + &RuntimeProbeTask::probeFinished, + this, + [this](const RuntimeProbeTask::ProbeReport& report) { m_results << report; }); + job->addTask(Task::Ptr(probe)); + token++; + } + + m_job->start(); + } + + void RuntimeCatalogTask::probeFinished() + { + QList candidates; + std::sort(m_results.begin(), + m_results.end(), + [](const RuntimeProbeTask::ProbeReport& a, const RuntimeProbeTask::ProbeReport& b) + { return a.token < b.token; }); + + qDebug() << "Found the following valid Java installations:"; + for (const auto& result : m_results) + { + if (result.status == RuntimeProbeTask::ProbeReport::Status::Valid) + { + RuntimeInstallPtr runtime(new RuntimeInstall()); + runtime->version = result.version; + runtime->arch = result.platformArch; + runtime->path = result.path; + runtime->vendor = result.vendor; + runtime->is_64bit = result.is_64bit; + runtime->managed = (m_scope == RuntimeCatalog::Scope::ManagedOnly); + candidates.append(runtime); + + qDebug() << " " << runtime->version.toString() << runtime->arch << runtime->path; + } + } + + QList entries; + for (const auto& runtime : candidates) + { + BaseVersion::Ptr base = std::dynamic_pointer_cast(runtime); + if (base) + { + entries.append(runtime); + } + } + + m_catalog->updateListData(entries); + emitSucceeded(); + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/services/RuntimeCatalog.hpp b/archived/projt-launcher/launcher/java/services/RuntimeCatalog.hpp new file mode 100644 index 0000000000..ddb2fc53a4 --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeCatalog.hpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +#include "BaseVersionList.h" +#include "QObjectPtr.h" +#include "java/core/RuntimeInstall.hpp" +#include "java/services/RuntimeProbeTask.hpp" +#include "tasks/Task.h" + +namespace projt::java +{ + class RuntimeCatalogTask; + + class RuntimeCatalog : public BaseVersionList + { + Q_OBJECT + enum class State + { + Idle, + Loading, + Ready + }; + + public: + enum class Scope + { + All, + ManagedOnly + }; + + explicit RuntimeCatalog(QObject* parent = nullptr, Scope scope = Scope::All); + + Task::Ptr getLoadTask() override; + bool isLoaded() override; + const BaseVersion::Ptr at(int i) const override; + int count() const override; + void sortVersions() override; + + QVariant data(const QModelIndex& index, int role) const override; + RoleList providesRoles() const override; + + public slots: + void updateListData(QList versions) override; + + private: + void load(); + Task::Ptr currentTask() const; + + State m_state = State::Idle; + shared_qobject_ptr m_task; + QList m_entries; + Scope m_scope; + }; + + class RuntimeCatalogTask : public Task + { + Q_OBJECT + + public: + explicit RuntimeCatalogTask(RuntimeCatalog* catalog, RuntimeCatalog::Scope scope); + ~RuntimeCatalogTask() override = default; + + protected: + void executeTask() override; + + public slots: + void probeFinished(); + + private: + Task::Ptr m_job; + RuntimeCatalog* m_catalog = nullptr; + QList m_results; + RuntimeCatalog::Scope m_scope; + }; +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/services/RuntimeEnvironment.cpp b/archived/projt-launcher/launcher/java/services/RuntimeEnvironment.cpp new file mode 100644 index 0000000000..e6f027e462 --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeEnvironment.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "java/services/RuntimeEnvironment.hpp" + +#include + +#define IBUS "@im=ibus" + +namespace projt::java +{ + QString stripEnvEntries(const QString& name, const QString& value, const QString& remove) + { + QChar delimiter = ':'; +#ifdef Q_OS_WIN32 + delimiter = ';'; +#endif + + QStringList targetItems = value.split(delimiter); + QStringList toRemove = remove.split(delimiter); + + for (const QString& item : toRemove) + { + if (!targetItems.removeOne(item)) + { + qWarning() << "Entry" << item << "could not be stripped from variable" << name; + } + } + return targetItems.join(delimiter); + } + + QProcessEnvironment buildCleanEnvironment() + { + QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); + QProcessEnvironment env; + + QStringList ignored = { "JAVA_ARGS", "CLASSPATH", "CONFIGPATH", "JAVA_HOME", + "JRE_HOME", "_JAVA_OPTIONS", "JAVA_OPTIONS", "JAVA_TOOL_OPTIONS" }; + + QStringList stripped = { +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + "LD_LIBRARY_PATH", + "LD_PRELOAD", +#endif + "QT_PLUGIN_PATH", + "QT_FONTPATH" + }; + + for (const auto& key : rawenv.keys()) + { + auto current = rawenv.value(key); + if (ignored.contains(key)) + { + qDebug() << "Env: ignoring" << key << current; + continue; + } + if (key.startsWith("LAUNCHER_")) + { + qDebug() << "Env: ignoring" << key << current; + continue; + } + if (stripped.contains(key)) + { + QString cleaned = stripEnvEntries(key, current, rawenv.value("LAUNCHER_" + key)); + qDebug() << "Env: stripped" << key << current << "to" << cleaned; + current = cleaned; + } +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (key == "XMODIFIERS" && current.contains(IBUS)) + { + QString saved = current; + current.replace(IBUS, ""); + qDebug() << "Env: stripped" << IBUS << "from" << saved << ":" << current; + } +#endif + env.insert(key, current); + } +#ifdef Q_OS_LINUX + if (!env.contains("LD_LIBRARY_PATH")) + { + env.insert("LD_LIBRARY_PATH", ""); + } +#endif + + return env; + } +} // namespace projt::java \ No newline at end of file diff --git a/archived/projt-launcher/launcher/java/services/RuntimeEnvironment.hpp b/archived/projt-launcher/launcher/java/services/RuntimeEnvironment.hpp new file mode 100644 index 0000000000..7101fc51ad --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeEnvironment.hpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +namespace projt::java +{ + QString stripEnvEntries(const QString& name, const QString& value, const QString& remove); + QProcessEnvironment buildCleanEnvironment(); +} // namespace projt::java \ No newline at end of file diff --git a/archived/projt-launcher/launcher/java/services/RuntimeProbeTask.cpp b/archived/projt-launcher/launcher/java/services/RuntimeProbeTask.cpp new file mode 100644 index 0000000000..b13b8d7ece --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeProbeTask.cpp @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "java/services/RuntimeProbeTask.hpp" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "Commandline.h" +#include "FileSystem.h" +#include "java/services/RuntimeEnvironment.hpp" + +namespace projt::java +{ + RuntimeProbeTask::RuntimeProbeTask(ProbeSettings settings) : Task(), m_settings(std::move(settings)) + {} + + QString RuntimeProbeTask::probeJarPath() + { + return APPLICATION->getJarPath("JavaCheck.jar"); + } + + void RuntimeProbeTask::executeTask() + { + QString checkerJar = probeJarPath(); + + if (checkerJar.isEmpty()) + { + qDebug() << "Java checker library could not be found. Please check your installation."; + ProbeReport report; + report.path = m_settings.binaryPath; + report.token = m_settings.token; + report.status = ProbeReport::Status::Errored; + emit probeFinished(report); + emitSucceeded(); + return; + } +#ifdef Q_OS_WIN + checkerJar = FS::getPathNameInLocal8bit(checkerJar); +#endif + + QStringList args; + process.reset(new QProcess()); + if (!m_settings.extraArgs.isEmpty()) + { + args.append(Commandline::splitArgs(m_settings.extraArgs)); + } + if (m_settings.minMem != 0) + { + args << QString("-Xms%1m").arg(m_settings.minMem); + } + if (m_settings.maxMem != 0) + { + args << QString("-Xmx%1m").arg(m_settings.maxMem); + } + if (m_settings.permGen != 64 && m_settings.permGen != 0) + { + args << QString("-XX:PermSize=%1m").arg(m_settings.permGen); + } + + args.append({ "-jar", checkerJar, "--list", "os.arch", "java.version", "java.vendor" }); + process->setArguments(args); + process->setProgram(m_settings.binaryPath); + process->setProcessChannelMode(QProcess::SeparateChannels); + process->setProcessEnvironment(buildCleanEnvironment()); + qDebug() << "Running runtime probe:" << m_settings.binaryPath << args.join(" "); + + connect(process.get(), &QProcess::finished, this, &RuntimeProbeTask::finished); + connect(process.get(), &QProcess::errorOccurred, this, &RuntimeProbeTask::error); + connect(process.get(), &QProcess::readyReadStandardOutput, this, &RuntimeProbeTask::stdoutReady); + connect(process.get(), &QProcess::readyReadStandardError, this, &RuntimeProbeTask::stderrReady); + connect(&killTimer, &QTimer::timeout, this, &RuntimeProbeTask::timeout); + killTimer.setSingleShot(true); + killTimer.start(15000); + process->start(); + } + + void RuntimeProbeTask::stdoutReady() + { + QByteArray data = process->readAllStandardOutput(); + QString added = QString::fromLocal8Bit(data); + added.remove('\r'); + m_stdout += added; + } + + void RuntimeProbeTask::stderrReady() + { + QByteArray data = process->readAllStandardError(); + QString added = QString::fromLocal8Bit(data); + added.remove('\r'); + m_stderr += added; + } + + void RuntimeProbeTask::finished(int exitcode, QProcess::ExitStatus status) + { + killTimer.stop(); + QProcessPtr activeProcess = process; + process.reset(); + + ProbeReport report; + report.path = m_settings.binaryPath; + report.token = m_settings.token; + report.stderrLog = m_stderr; + report.stdoutLog = m_stdout; + qDebug() << "STDOUT" << m_stdout; + if (!m_stderr.isEmpty()) + { + qWarning() << "STDERR" << m_stderr; + } + qDebug() << "Runtime probe finished with status" << status << "exit code" << exitcode; + + if (status == QProcess::CrashExit) + { + report.status = ProbeReport::Status::Errored; + emit probeFinished(report); + emitSucceeded(); + return; + } + + auto parseBlocks = [](const QString& stdoutText) + { + QList> blocks; + QMap current; + const auto lines = stdoutText.split('\n'); + for (QString line : lines) + { + line = line.trimmed(); + if (line.isEmpty()) + { + if (!current.isEmpty()) + { + blocks.append(current); + current.clear(); + } + continue; + } + if (line.contains("/bedrock/strata")) + { + continue; + } + const auto eq = line.indexOf('='); + if (eq <= 0) + { + continue; + } + const auto key = line.left(eq).trimmed(); + const auto value = line.mid(eq + 1).trimmed(); + if (!key.isEmpty() && !value.isEmpty()) + { + current.insert(key, value); + } + } + if (!current.isEmpty()) + { + blocks.append(current); + } + return blocks; + }; + + auto resolvePath = [](const QString& path) + { + if (path.isEmpty()) + { + return QString(); + } + auto resolved = FS::ResolveExecutable(path); + QString chosen = resolved.isEmpty() ? path : resolved; + QFileInfo info(chosen); + if (info.exists()) + { + return info.canonicalFilePath(); + } + return chosen; + }; + + auto matchesProbeTarget = [](const QString& targetPath, const QString& candidatePath) + { + if (targetPath.isEmpty() || candidatePath.isEmpty()) + { + return false; + } + if (candidatePath == targetPath) + { + return true; + } +#ifdef Q_OS_WIN + const QFileInfo targetInfo(targetPath); + const QFileInfo candidateInfo(candidatePath); + auto normalizeJavaName = [](QString fileName) + { + fileName = fileName.toLower(); + if (fileName == "javaw.exe") + { + return QStringLiteral("java.exe"); + } + return fileName; + }; + + return targetInfo.absolutePath().compare(candidateInfo.absolutePath(), Qt::CaseInsensitive) == 0 + && normalizeJavaName(targetInfo.fileName()) == normalizeJavaName(candidateInfo.fileName()); +#else + return false; +#endif + }; + + const auto targetPath = resolvePath(m_settings.binaryPath); + QMap results; + auto blocks = parseBlocks(m_stdout); + for (const auto& block : blocks) + { + const auto candidatePath = resolvePath(block.value("java.path")); + if (matchesProbeTarget(targetPath, candidatePath)) + { + results = block; + break; + } + } + if (results.isEmpty() && !blocks.isEmpty() && targetPath.isEmpty()) + { + results = blocks.first(); + } + + if (results.isEmpty() || !results.contains("os.arch") || !results.contains("java.version") + || !results.contains("java.vendor")) + { + report.status = ProbeReport::Status::InvalidData; + emit probeFinished(report); + emitSucceeded(); + return; + } + + auto osArch = results["os.arch"]; + auto javaVersion = results["java.version"]; + auto javaVendor = results["java.vendor"]; + bool is64 = osArch == "x86_64" || osArch == "amd64" || osArch == "aarch64" || osArch == "arm64" + || osArch == "riscv64" || osArch == "ppc64le" || osArch == "ppc64"; + + report.status = ProbeReport::Status::Valid; + report.is_64bit = is64; + report.platformTag = is64 ? "64" : "32"; + report.platformArch = osArch; + report.version = javaVersion; + report.vendor = javaVendor; + qDebug() << "Runtime probe succeeded."; + emit probeFinished(report); + emitSucceeded(); + } + + void RuntimeProbeTask::error(QProcess::ProcessError err) + { + if (err == QProcess::FailedToStart) + { + qDebug() << "Runtime probe failed to start."; + qDebug() << "Process environment:"; + qDebug() << process->environment(); + qDebug() << "Native environment:"; + qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); + killTimer.stop(); + ProbeReport report; + report.path = m_settings.binaryPath; + report.token = m_settings.token; + emit probeFinished(report); + } + emitSucceeded(); + } + + void RuntimeProbeTask::timeout() + { + if (process) + { + qDebug() << "Runtime probe has been killed by timeout."; + process->kill(); + } + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/services/RuntimeProbeTask.hpp b/archived/projt-launcher/launcher/java/services/RuntimeProbeTask.hpp new file mode 100644 index 0000000000..c2b78a4c57 --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeProbeTask.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +#include "QObjectPtr.h" +#include "java/core/RuntimeVersion.hpp" +#include "tasks/Task.h" + +namespace projt::java +{ + class RuntimeProbeTask : public Task + { + Q_OBJECT + public: + using QProcessPtr = shared_qobject_ptr; + using Ptr = shared_qobject_ptr; + + struct ProbeSettings + { + QString binaryPath; + QString extraArgs; + int minMem = 0; + int maxMem = 0; + int permGen = 0; + int token = 0; + }; + + struct ProbeReport + { + QString path; + int token = 0; + QString platformTag; + QString platformArch; + RuntimeVersion version; + QString vendor; + QString stdoutLog; + QString stderrLog; + bool is_64bit = false; + enum class Status + { + Errored, + InvalidData, + Valid + } status = Status::Errored; + }; + + explicit RuntimeProbeTask(ProbeSettings settings); + + static QString probeJarPath(); + + signals: + void probeFinished(const ProbeReport& report); + + protected: + void executeTask() override; + + private: + QProcessPtr process; + QTimer killTimer; + QString m_stdout; + QString m_stderr; + + ProbeSettings m_settings; + + private slots: + void timeout(); + void finished(int exitcode, QProcess::ExitStatus status); + void error(QProcess::ProcessError err); + void stdoutReady(); + void stderrReady(); + }; +} // namespace projt::java \ No newline at end of file diff --git a/archived/projt-launcher/launcher/java/services/RuntimeScanner.cpp b/archived/projt-launcher/launcher/java/services/RuntimeScanner.cpp new file mode 100644 index 0000000000..5aed027d78 --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeScanner.cpp @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "java/services/RuntimeScanner.hpp" + +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "FileSystem.h" + +namespace projt::java +{ + static QStringList resolveExecutables(const QStringList& candidates) + { + QStringList resolved; + resolved.reserve(candidates.size()); + for (const auto& candidate : candidates) + { + if (candidate.isEmpty()) + { + continue; + } + if (QDir::isRelativePath(candidate)) + { + const auto found = QStandardPaths::findExecutable(candidate); + if (!found.isEmpty()) + { + resolved.append(QFileInfo(found).canonicalFilePath()); + } + continue; + } + const QFileInfo info(candidate); + if (info.exists() && info.isFile()) + { + resolved.append(info.canonicalFilePath()); + } + } + resolved.removeDuplicates(); + return resolved; + } + + QString RuntimeScanner::executableName() + { +#if defined(Q_OS_WIN32) + return QStringLiteral("javaw.exe"); +#else + return QStringLiteral("java"); +#endif + } + + QStringList RuntimeScanner::appendEnvPaths(const QStringList& base) const + { + QStringList expanded = base; + auto env = qEnvironmentVariable("PROJTLAUNCHER_JAVA_PATHS"); +#if defined(Q_OS_WIN32) + QStringList javaPaths = env.replace("\\", "/").split(QLatin1String(";"), Qt::SkipEmptyParts); + + auto envPath = qEnvironmentVariable("PATH"); + QStringList pathEntries = envPath.replace("\\", "/").split(QLatin1String(";"), Qt::SkipEmptyParts); + for (const QString& entry : pathEntries) + { + javaPaths.append(entry + "/" + executableName()); + } +#else + QStringList javaPaths = env.split(QLatin1String(":"), Qt::SkipEmptyParts); +#endif + for (const QString& entry : javaPaths) + { + expanded.append(entry); + } + return expanded; + } + +#if defined(Q_OS_WIN32) + QStringList RuntimeScanner::collectRegistryPaths(DWORD keyType, + const QString& keyName, + const QString& valueName, + const QString& suffix) const + { + QStringList entries; + + for (HKEY baseRegistry : { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE }) + { + HKEY rootKey; + if (RegOpenKeyExW(baseRegistry, + keyName.toStdWString().c_str(), + 0, + KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, + &rootKey) + != ERROR_SUCCESS) + { + continue; + } + + DWORD subKeyCount = 0; + RegQueryInfoKeyW(rootKey, NULL, NULL, NULL, &subKeyCount, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + if (subKeyCount > 0) + { + for (DWORD i = 0; i < subKeyCount; i++) + { + WCHAR subKeyName[255]; + DWORD subKeyNameSize = 255; + auto retCode = RegEnumKeyExW(rootKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, NULL); + if (retCode != ERROR_SUCCESS) + { + continue; + } + + QString versionKey = keyName + "\\" + QString::fromWCharArray(subKeyName) + suffix; + HKEY versionHandle; + if (RegOpenKeyExW(baseRegistry, + versionKey.toStdWString().c_str(), + 0, + KEY_READ | keyType, + &versionHandle) + != ERROR_SUCCESS) + { + continue; + } + + DWORD valueSize = 0; + if (RegQueryValueExW(versionHandle, valueName.toStdWString().c_str(), NULL, NULL, NULL, &valueSize) + == ERROR_SUCCESS) + { + std::vector buffer(valueSize / sizeof(WCHAR) + 1, 0); + RegQueryValueExW(versionHandle, + valueName.toStdWString().c_str(), + NULL, + NULL, + reinterpret_cast(buffer.data()), + &valueSize); + QString javaHome = QString::fromWCharArray(buffer.data()); + QString javaPath = QDir(FS::PathCombine(javaHome, "bin")).absoluteFilePath(executableName()); + entries.append(javaPath); + } + RegCloseKey(versionHandle); + } + } + + RegCloseKey(rootKey); + } + return entries; + } +#endif + + QStringList RuntimeScanner::collectManagedBundles() + { + QStringList bundles; + + auto addBundlePaths = [&bundles](const QString& prefix) + { + bundles.append(FS::PathCombine(prefix, "jre", "bin", RuntimeScanner::executableName())); + bundles.append(FS::PathCombine(prefix, "bin", RuntimeScanner::executableName())); + bundles.append(FS::PathCombine(prefix, RuntimeScanner::executableName())); + }; + + auto scanJavaDir = [&addBundlePaths](const QString& dirPath) + { + QDir dir(dirPath); + if (!dir.exists()) + { + return; + } + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const auto& entry : entries) + { + addBundlePaths(entry.canonicalFilePath()); + } + }; + + scanJavaDir(APPLICATION->javaPath()); + + return bundles; + } + + QStringList RuntimeScanner::collectMinecraftBundles() + { + QStringList processPaths; +#if defined(Q_OS_MACOS) + processPaths << FS::PathCombine(QDir::homePath(), + FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); +#elif defined(Q_OS_WIN32) + auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", ""); + processPaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime"); + + auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); + auto minecraftStorePath = FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), + "Packages", + "Microsoft.4297127D64EC6_8wekyb3d8bbwe"); + processPaths << FS::PathCombine(minecraftStorePath, "LocalCache", "Local", "runtime"); +#else + processPaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime"); +#endif + + QStringList results; + while (!processPaths.isEmpty()) + { + auto dirPath = processPaths.takeFirst(); + QDir dir(dirPath); + if (!dir.exists()) + { + continue; + } + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + bool foundBin = false; + for (const auto& entry : entries) + { + if (entry.baseName() == "bin") + { + results.append(FS::PathCombine(entry.canonicalFilePath(), RuntimeScanner::executableName())); + foundBin = true; + break; + } + } + if (!foundBin) + { + for (const auto& entry : entries) + { + processPaths << entry.canonicalFilePath(); + } + } + } + return results; + } + + QStringList RuntimeScanner::collectPaths(bool managedOnly) const + { + QStringList candidates; + if (managedOnly) + { + candidates = collectManagedBundles(); + return resolveExecutables(candidates); + } + +#if defined(Q_OS_WIN32) + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome", "")); + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome", "")); + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome", "")); + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome", "")); + + candidates.append(collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome", "")); + candidates.append(collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome", "")); + candidates.append(collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome", "")); + candidates.append(collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome", "")); + + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI")); + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI")); + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI")); + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI")); + + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI")); + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI")); + + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI")); + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI")); + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI")); + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI")); + + candidates.append(collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI")); + candidates.append(collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI")); + candidates.append(collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI")); + candidates.append(collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI")); + + candidates.append(collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI")); + + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath", "")); + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath", "")); + + candidates.append( + collectRegistryPaths(KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath", "")); + candidates.append( + collectRegistryPaths(KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath", "")); + + candidates.append("C:/Program Files/Java/jre8/bin/javaw.exe"); + candidates.append("C:/Program Files/Java/jre7/bin/javaw.exe"); + candidates.append("C:/Program Files/Java/jre6/bin/javaw.exe"); + candidates.append("C:/Program Files (x86)/Java/jre8/bin/javaw.exe"); + candidates.append("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"); + candidates.append("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"); + + candidates.append("javaw"); +#elif defined(Q_OS_MAC) + candidates.append("java"); + candidates.append( + "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"); + candidates.append("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"); + candidates.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); + + QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); + QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& java : libraryJVMJavas) + { + candidates.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); + candidates.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); + } + + QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); + QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& java : systemLibraryJVMJavas) + { + candidates.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); + candidates.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); + } + + auto home = qEnvironmentVariable("HOME"); + + QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); + QDir sdkmanJavaDir(FS::PathCombine(sdkmanDir, "candidates/java")); + QStringList sdkmanJavas = sdkmanJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& java : sdkmanJavas) + { + candidates.append(sdkmanJavaDir.absolutePath() + "/" + java + "/bin/java"); + } + + QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); + QDir asdfJavaDir(FS::PathCombine(asdfDataDir, "installs/java")); + QStringList asdfJavas = asdfJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& java : asdfJavas) + { + candidates.append(asdfJavaDir.absolutePath() + "/" + java + "/bin/java"); + } + + QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); + QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& java : userLibraryJVMJavas) + { + candidates.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); + candidates.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) + candidates.append("java"); + auto scanJavaDir = + [&candidates]( + const QString& dirPath, + const std::function& filter = [](const QFileInfo&) { return true; }) + { + QDir dir(dirPath); + if (!dir.exists()) + return; + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const auto& entry : entries) + { + if (!filter(entry)) + continue; + QString prefix = entry.canonicalFilePath(); + candidates.append(FS::PathCombine(prefix, "jre/bin/java")); + candidates.append(FS::PathCombine(prefix, "bin/java")); + } + }; + auto snap = qEnvironmentVariable("SNAP"); + auto scanJavaDirs = [scanJavaDir, snap](const QString& dirPath) + { + scanJavaDir(dirPath); + if (!snap.isNull()) + { + scanJavaDir(snap + dirPath); + } + }; +#if defined(Q_OS_LINUX) + scanJavaDirs("/usr/java"); + scanJavaDirs("/usr/lib/jvm"); + scanJavaDirs("/usr/lib64/jvm"); + scanJavaDirs("/usr/lib32/jvm"); + auto gentooFilter = [](const QFileInfo& info) + { + QString fileName = info.fileName(); + return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-"); + }; + auto aoscFilter = [](const QFileInfo& info) + { + QString fileName = info.fileName(); + return fileName == "java" || fileName.startsWith("java-"); + }; + scanJavaDir("/usr/lib64", gentooFilter); + scanJavaDir("/usr/lib", gentooFilter); + scanJavaDir("/opt", gentooFilter); + scanJavaDir("/usr/lib", aoscFilter); + scanJavaDirs("java"); + scanJavaDirs("/opt/jdk"); + scanJavaDirs("/opt/jdks"); + scanJavaDirs("/opt/ibm"); + scanJavaDirs("/app/jdk"); +#elif defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) + scanJavaDirs("/usr/local"); +#endif + auto home = qEnvironmentVariable("HOME"); + scanJavaDirs(FS::PathCombine(home, ".jdks")); + QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); + scanJavaDirs(FS::PathCombine(sdkmanDir, "candidates/java")); + QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); + scanJavaDirs(FS::PathCombine(asdfDataDir, "installs/java")); + scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); +#else + candidates.append("java"); +#endif + + candidates.append(collectMinecraftBundles()); + candidates.append(collectManagedBundles()); + candidates = appendEnvPaths(candidates); + return resolveExecutables(candidates); + } +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/java/services/RuntimeScanner.hpp b/archived/projt-launcher/launcher/java/services/RuntimeScanner.hpp new file mode 100644 index 0000000000..09000c914d --- /dev/null +++ b/archived/projt-launcher/launcher/java/services/RuntimeScanner.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +#if defined(Q_OS_WIN32) +#include +#endif + +namespace projt::java +{ + class RuntimeScanner + { + public: + RuntimeScanner() = default; + + QStringList collectPaths(bool managedOnly) const; + + static QString executableName(); + static QStringList collectManagedBundles(); + static QStringList collectMinecraftBundles(); + + private: + QStringList appendEnvPaths(const QStringList& base) const; + +#if defined(Q_OS_WIN32) + QStringList collectRegistryPaths(DWORD keyType, + const QString& keyName, + const QString& valueName, + const QString& suffix) const; +#endif + }; +} // namespace projt::java diff --git a/archived/projt-launcher/launcher/launch/LaunchLineRouter.cpp b/archived/projt-launcher/launcher/launch/LaunchLineRouter.cpp new file mode 100644 index 0000000000..4280e749d3 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchLineRouter.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LaunchLineRouter.hpp" + +#include +#include + +#include "LaunchLogModel.hpp" + +namespace projt::launch +{ + QString LaunchLineRouter::applyRedactions(QString input, const QMap& redactions) const + { + for (auto it = redactions.begin(); it != redactions.end(); ++it) + { + input.replace(it.key(), it.value()); + } + return input; + } + + bool LaunchLineRouter::parseStructured(const QString& line, + MessageLevel::Enum level, + LaunchLogModel& model, + const QMap& redactions) + { + projt::logs::LogEventParser* parser = nullptr; + switch (level) + { + case MessageLevel::StdErr: parser = &m_stderrParser; break; + case MessageLevel::StdOut: parser = &m_stdoutParser; break; + default: return false; + } + + parser->pushLine(line); + auto items = parser->drainAvailable(); + if (auto err = parser->lastError(); err.has_value()) + { + model.append(MessageLevel::Error, QObject::tr("Log parser error: %1").arg(err.value().message)); + return false; + } + + if (items.isEmpty()) + { + return true; + } + + for (const auto& item : items) + { + if (std::holds_alternative(item)) + { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + msg = applyRedactions(msg, redactions); + model.append(entry.level, msg); + continue; + } + if (std::holds_alternative(item)) + { + auto msg = std::get(item).text; + auto resolved = projt::logs::LogEventParser::guessLevelFromLine(msg, model.previousLevel()); + msg = applyRedactions(msg, redactions); + model.append(resolved, msg); + } + } + + return true; + } + + void LaunchLineRouter::routeLine(QString line, + MessageLevel::Enum defaultLevel, + LaunchLogModel& model, + const QMap& redactions) + { + if (parseStructured(line, defaultLevel, model, redactions)) + { + return; + } + + auto embedded = MessageLevel::fromLine(line); + if (embedded != MessageLevel::Unknown) + { + defaultLevel = embedded; + } + + if (defaultLevel == MessageLevel::Unknown) + { + defaultLevel = projt::logs::LogEventParser::guessLevelFromLine(line, model.previousLevel()); + } + + line = applyRedactions(line, redactions); + model.append(defaultLevel, line); + } +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchLineRouter.hpp b/archived/projt-launcher/launcher/launch/LaunchLineRouter.hpp new file mode 100644 index 0000000000..812257c815 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchLineRouter.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "MessageLevel.h" +#include "logs/LogEventParser.hpp" + +namespace projt::launch +{ + class LaunchLogModel; + + class LaunchLineRouter + { + public: + void routeLine(QString line, + MessageLevel::Enum defaultLevel, + LaunchLogModel& model, + const QMap& redactions); + + private: + bool parseStructured(const QString& line, + MessageLevel::Enum level, + LaunchLogModel& model, + const QMap& redactions); + QString applyRedactions(QString input, const QMap& redactions) const; + + projt::logs::LogEventParser m_stdoutParser; + projt::logs::LogEventParser m_stderrParser; + }; +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchLogModel.cpp b/archived/projt-launcher/launcher/launch/LaunchLogModel.cpp new file mode 100644 index 0000000000..84fd6c1d36 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchLogModel.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LaunchLogModel.hpp" + +namespace projt::launch +{ + LaunchLogModel::LaunchLogModel(QObject* parent) : QAbstractListModel(parent) + { + m_buffer.resize(m_limit); + updateDefaultOverflowMessage(); + } + + int LaunchLogModel::rowCount(const QModelIndex& parent) const + { + if (parent.isValid()) + { + return 0; + } + return m_size; + } + + QVariant LaunchLogModel::data(const QModelIndex& index, int role) const + { + if (!index.isValid()) + { + return {}; + } + if (index.row() < 0 || index.row() >= m_size) + { + return {}; + } + + int realIndex = bufferIndex(index.row()); + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + return m_buffer[realIndex].line; + } + if (role == LevelRole) + { + return m_buffer[realIndex].level; + } + return {}; + } + + void LaunchLogModel::append(MessageLevel::Enum level, QString line) + { + if (m_paused) + { + return; + } + if (m_size == m_limit) + { + if (m_hardLimit) + { + return; + } + beginRemoveRows(QModelIndex(), 0, 0); + m_offset = (m_offset + 1) % m_limit; + m_size--; + endRemoveRows(); + } + else if (m_hardLimit && m_size == m_limit - 1) + { + level = MessageLevel::Fatal; + line = m_overflowText; + } + + int insertRow = m_size; + int slot = (m_offset + m_size) % m_limit; + + beginInsertRows(QModelIndex(), insertRow, insertRow); + m_buffer[slot].level = level; + m_buffer[slot].line = line; + m_size++; + endInsertRows(); + } + + void LaunchLogModel::suspend(bool suspend) + { + m_paused = suspend; + } + + bool LaunchLogModel::suspended() const + { + return m_paused; + } + + void LaunchLogModel::clear() + { + beginResetModel(); + m_offset = 0; + m_size = 0; + endResetModel(); + } + + QString LaunchLogModel::toPlainText() const + { + QString out; + out.reserve(m_size * 80); + for (int i = 0; i < m_size; ++i) + { + out.append(m_buffer[bufferIndex(i)].line); + out.append('\n'); + } + out.squeeze(); + return out; + } + + void LaunchLogModel::setMaxLines(int maxLines) + { + if (maxLines == m_limit || maxLines <= 0) + { + return; + } + + beginResetModel(); + QVector newRing; + newRing.resize(maxLines); + int keep = qMin(m_size, maxLines); + for (int i = 0; i < keep; ++i) + { + int src = (m_offset + m_size - keep + i) % m_limit; + newRing[i] = m_buffer[src]; + } + m_buffer.swap(newRing); + m_limit = maxLines; + m_offset = 0; + m_size = keep; + endResetModel(); + } + + int LaunchLogModel::getMaxLines() const + { + return m_limit; + } + + void LaunchLogModel::setStopOnOverflow(bool stop) + { + m_hardLimit = stop; + } + + void LaunchLogModel::setOverflowMessage(const QString& overflowMessage) + { + m_overflowText = overflowMessage; + } + + bool LaunchLogModel::isOverflow() const + { + return m_size >= m_limit && m_hardLimit; + } + + void LaunchLogModel::setLineWrap(bool state) + { + m_wrapLines = state; + } + + bool LaunchLogModel::wrapLines() const + { + return m_wrapLines; + } + + void LaunchLogModel::setColorLines(bool state) + { + m_colorize = state; + } + + bool LaunchLogModel::colorLines() const + { + return m_colorize; + } + + MessageLevel::Enum LaunchLogModel::previousLevel() const + { + if (m_size == 0) + { + return MessageLevel::Unknown; + } + int lastIndex = bufferIndex(m_size - 1); + return m_buffer[lastIndex].level; + } + + void LaunchLogModel::updateDefaultOverflowMessage() + { + m_overflowText = tr("Stopped watching the game log because the log length surpassed %1 lines.\n" + "You may have to fix your mods because the game is still logging to files and" + " likely wasting harddrive space at an alarming rate!") + .arg(m_limit); + } + + int LaunchLogModel::bufferIndex(int row) const + { + return (m_offset + row) % m_limit; + } +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchLogModel.hpp b/archived/projt-launcher/launcher/launch/LaunchLogModel.hpp new file mode 100644 index 0000000000..5c9d15d5d6 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchLogModel.hpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "MessageLevel.h" + +namespace projt::launch +{ + class LaunchLogModel : public QAbstractListModel + { + Q_OBJECT + public: + explicit LaunchLogModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + + void append(MessageLevel::Enum level, QString line); + void clear(); + + void suspend(bool suspend); + bool suspended() const; + + QString toPlainText() const; + + int getMaxLines() const; + void setMaxLines(int maxLines); + void setStopOnOverflow(bool stop); + void setOverflowMessage(const QString& overflowMessage); + bool isOverflow() const; + + void setLineWrap(bool state); + bool wrapLines() const; + void setColorLines(bool state); + bool colorLines() const; + + MessageLevel::Enum previousLevel() const; + + enum Roles + { + LevelRole = Qt::UserRole + }; + + private: + struct LogEntry + { + MessageLevel::Enum level = MessageLevel::Enum::Unknown; + QString line; + }; + + void updateDefaultOverflowMessage(); + int bufferIndex(int row) const; + + QVector m_buffer; + int m_limit = 1000; + int m_offset = 0; + int m_size = 0; + bool m_hardLimit = false; + QString m_overflowText = "OVERFLOW"; + bool m_paused = false; + bool m_wrapLines = true; + bool m_colorize = true; + }; +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchPipeline.cpp b/archived/projt-launcher/launcher/launch/LaunchPipeline.cpp new file mode 100644 index 0000000000..e4a8650b81 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchPipeline.cpp @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LaunchPipeline.hpp" + +#include + +#include "BaseInstance.h" + +namespace projt::launch +{ + LaunchPipeline::Ptr LaunchPipeline::create(MinecraftInstancePtr instance) + { + return LaunchPipeline::Ptr(new LaunchPipeline(std::move(instance))); + } + + LaunchPipeline::LaunchPipeline(MinecraftInstancePtr instance) : m_instanceRef(std::move(instance)) + { + if (m_instanceRef) + { + m_instanceRef->setRunning(true); + } + } + + void LaunchPipeline::appendStage(StagePtr stage) + { + m_stageQueue.append(stage); + } + + void LaunchPipeline::prependStage(StagePtr stage) + { + m_stageQueue.prepend(stage); + } + + void LaunchPipeline::executeTask() + { + if (!m_instanceRef) + { + emitFailed(tr("Missing instance for launch.")); + return; + } + m_instanceRef->setCrashed(false); + if (m_stageQueue.isEmpty()) + { + m_state = State::Finished; + emitSucceeded(); + return; + } + m_state = State::Running; + advanceStage(); + } + + void LaunchPipeline::advanceStage() + { + if (m_activeStage && !m_activeStage->wasSuccessful()) + { + closeStages(false, m_activeStage->failReason()); + return; + } + + if (m_stageQueue.isEmpty()) + { + closeStages(true, QString()); + return; + } + + m_activeStage = m_stageQueue.takeFirst(); + m_stageHistory.append(m_activeStage); + m_activeStage->start(); + } + + void LaunchPipeline::onReadyForLaunch() + { + m_state = State::Waiting; + emit readyForLaunch(); + } + + void LaunchPipeline::onStageFinished() + { + advanceStage(); + } + + void LaunchPipeline::closeStages(bool successful, const QString& error) + { + for (int idx = m_stageHistory.size() - 1; idx >= 0; --idx) + { + m_stageHistory[idx]->finalize(); + } + m_activeStage.reset(); + if (successful) + { + m_state = State::Finished; + emitSucceeded(); + } + else + { + m_state = State::Failed; + emitFailed(error); + } + } + + void LaunchPipeline::onProgressReportingRequested() + { + if (!m_activeStage) + { + return; + } + m_state = State::Waiting; + emit requestProgress(m_activeStage.get()); + } + + void LaunchPipeline::setCensorFilter(QMap filter) + { + m_redactions = std::move(filter); + } + + QString LaunchPipeline::censorPrivateInfo(QString input) const + { + for (auto it = m_redactions.begin(); it != m_redactions.end(); ++it) + { + input.replace(it.key(), it.value()); + } + return input; + } + + void LaunchPipeline::proceed() + { + if (m_state != State::Waiting) + { + return; + } + if (!m_activeStage) + { + return; + } + m_activeStage->proceed(); + } + + bool LaunchPipeline::canAbort() const + { + switch (m_state) + { + case State::Aborted: + case State::Failed: + case State::Finished: return false; + case State::Idle: return true; + case State::Running: + case State::Waiting: + { + if (!m_activeStage) + { + return false; + } + return m_activeStage->canAbort(); + } + } + return false; + } + + bool LaunchPipeline::abort() + { + switch (m_state) + { + case State::Aborted: + case State::Failed: + case State::Finished: return true; + case State::Idle: + { + m_state = State::Aborted; + emitAborted(); + return true; + } + case State::Running: + case State::Waiting: + { + if (!m_activeStage) + { + return false; + } + if (!m_activeStage->canAbort()) + { + return false; + } + if (m_activeStage->abort()) + { + m_state = State::Aborted; + return true; + } + break; + } + } + return false; + } + + shared_qobject_ptr LaunchPipeline::logModel() + { + if (!m_logStore) + { + m_logStore.reset(new LaunchLogModel()); + m_logStore->setMaxLines(getConsoleMaxLines(m_instanceRef->settings())); + m_logStore->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instanceRef->settings())); + m_logStore->setOverflowMessage( + tr("Stopped watching the game log because the log length surpassed %1 lines.\n" + "You may have to fix your mods because the game is still logging to files and" + " likely wasting harddrive space at an alarming rate!") + .arg(m_logStore->getMaxLines())); + } + return m_logStore; + } + + void LaunchPipeline::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel) + { + for (const auto& line : lines) + { + onLogLine(line, defaultLevel); + } + } + + void LaunchPipeline::onLogLine(QString line, MessageLevel::Enum level) + { + m_lineRouter.routeLine(std::move(line), level, *logModel(), m_redactions); + } + + void LaunchPipeline::emitSucceeded() + { + if (m_instanceRef) + { + m_instanceRef->setRunning(false); + } + Task::emitSucceeded(); + } + + void LaunchPipeline::emitFailed(QString reason) + { + if (m_instanceRef) + { + m_instanceRef->setRunning(false); + m_instanceRef->setCrashed(true); + } + Task::emitFailed(reason); + } + + QString LaunchPipeline::substituteVariables(const QString& cmd, bool isLaunch) const + { + if (!m_instanceRef) + { + return cmd; + } + auto env = isLaunch ? m_instanceRef->createLaunchEnvironment() : m_instanceRef->createEnvironment(); + return m_expander.expand(cmd, env); + } +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchPipeline.hpp b/archived/projt-launcher/launcher/launch/LaunchPipeline.hpp new file mode 100644 index 0000000000..53e9ec3bfe --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchPipeline.hpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "LaunchLineRouter.hpp" +#include "LaunchLogModel.hpp" +#include "LaunchStage.hpp" +#include "LaunchVariableExpander.hpp" +#include "MessageLevel.h" + +class BaseInstance; + +namespace projt::launch +{ + class LaunchPipeline : public Task + { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + using StagePtr = shared_qobject_ptr; + + enum class State + { + Idle, + Running, + Waiting, + Failed, + Aborted, + Finished + }; + + static Ptr create(MinecraftInstancePtr instance); + explicit LaunchPipeline(MinecraftInstancePtr instance); + ~LaunchPipeline() override = default; + + void appendStage(StagePtr stage); + void prependStage(StagePtr stage); + void setCensorFilter(QMap filter); + + MinecraftInstancePtr instance() const + { + return m_instanceRef; + } + + void setPid(qint64 pid) + { + m_processId = pid; + } + + qint64 pid() const + { + return m_processId; + } + + void executeTask() override; + void proceed(); + bool abort() override; + bool canAbort() const override; + + shared_qobject_ptr logModel(); + + QString substituteVariables(const QString& cmd, bool isLaunch = false) const; + QString censorPrivateInfo(QString input) const; + + signals: + void readyForLaunch(); + void requestProgress(Task* task); + + public slots: + void onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); + void onLogLine(QString line, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); + void onReadyForLaunch(); + void onStageFinished(); + void onProgressReportingRequested(); + + protected: + void emitFailed(QString reason) override; + void emitSucceeded() override; + + private: + void advanceStage(); + void closeStages(bool successful, const QString& error); + + MinecraftInstancePtr m_instanceRef; + shared_qobject_ptr m_logStore; + QList m_stageQueue; + QList m_stageHistory; + StagePtr m_activeStage; + QMap m_redactions; + State m_state = State::Idle; + qint64 m_processId = -1; + LaunchLineRouter m_lineRouter; + LaunchVariableExpander m_expander; + }; +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchStage.cpp b/archived/projt-launcher/launcher/launch/LaunchStage.cpp new file mode 100644 index 0000000000..3748d58bf2 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchStage.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LaunchStage.hpp" +#include "LaunchPipeline.hpp" + +namespace projt::launch +{ + LaunchStage::LaunchStage(LaunchPipeline* pipeline) : Task(), m_flow(pipeline) + { + if (!m_flow) + { + return; + } + connect(this, &LaunchStage::readyForLaunch, m_flow, &LaunchPipeline::onReadyForLaunch); + connect(this, &LaunchStage::logLine, m_flow, &LaunchPipeline::onLogLine); + connect(this, &LaunchStage::logLines, m_flow, &LaunchPipeline::onLogLines); + connect(this, &LaunchStage::finished, m_flow, &LaunchPipeline::onStageFinished); + connect(this, &LaunchStage::progressReportingRequest, m_flow, &LaunchPipeline::onProgressReportingRequested); + } +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchStage.hpp b/archived/projt-launcher/launcher/launch/LaunchStage.hpp new file mode 100644 index 0000000000..7c3e04c51f --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchStage.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "MessageLevel.h" +#include "tasks/Task.h" + +#include + +namespace projt::launch +{ + class LaunchPipeline; + + class LaunchStage : public Task + { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + explicit LaunchStage(LaunchPipeline* pipeline); + ~LaunchStage() override = default; + + LaunchPipeline* pipeline() const + { + return m_flow; + } + + signals: + void logLines(QStringList lines, MessageLevel::Enum level); + void logLine(QString line, MessageLevel::Enum level); + void readyForLaunch(); + void progressReportingRequest(); + + public slots: + virtual void proceed() {}; + virtual void finalize() {}; + + protected: + LaunchPipeline* m_flow; + }; +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchVariableExpander.cpp b/archived/projt-launcher/launcher/launch/LaunchVariableExpander.cpp new file mode 100644 index 0000000000..175b003115 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchVariableExpander.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LaunchVariableExpander.hpp" + +namespace projt::launch +{ + QString LaunchVariableExpander::expand(const QString& input, const QProcessEnvironment& env) const + { + QString output = input; + + enum class Mode + { + Idle, + Sigil, + Name, + Brace + }; + + Mode mode = Mode::Idle; + int tokenStart = -1; + + for (int idx = 0; idx < output.length();) + { + const QChar c = output.at(idx++); + switch (mode) + { + case Mode::Idle: + if (c == '$') + { + mode = Mode::Sigil; + } + break; + case Mode::Sigil: + if (c == '{') + { + mode = Mode::Brace; + tokenStart = idx; + } + else if (c.isLetterOrNumber() || c == '_') + { + mode = Mode::Name; + tokenStart = idx - 1; + } + else + { + mode = Mode::Idle; + } + break; + case Mode::Brace: + if (c == '}') + { + const auto key = output.mid(tokenStart, idx - 1 - tokenStart); + const auto value = env.value(key, ""); + if (!value.isEmpty()) + { + output.replace(tokenStart - 2, idx - tokenStart + 2, value); + idx = tokenStart - 2 + value.length(); + } + mode = Mode::Idle; + } + break; + case Mode::Name: + if (!c.isLetterOrNumber() && c != '_') + { + const auto key = output.mid(tokenStart, idx - tokenStart - 1); + const auto value = env.value(key, ""); + if (!value.isEmpty()) + { + output.replace(tokenStart - 1, idx - tokenStart, value); + idx = tokenStart - 1 + value.length(); + } + mode = Mode::Idle; + } + break; + } + } + + if (mode == Mode::Name) + { + const auto value = env.value(output.mid(tokenStart), ""); + if (!value.isEmpty()) + { + output.replace(tokenStart - 1, output.length() - tokenStart + 1, value); + } + } + + return output; + } +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/LaunchVariableExpander.hpp b/archived/projt-launcher/launcher/launch/LaunchVariableExpander.hpp new file mode 100644 index 0000000000..62d0782a77 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/LaunchVariableExpander.hpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +namespace projt::launch +{ + class LaunchVariableExpander + { + public: + QString expand(const QString& input, const QProcessEnvironment& env) const; + }; +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/TaskBridgeStage.cpp b/archived/projt-launcher/launcher/launch/TaskBridgeStage.cpp new file mode 100644 index 0000000000..0fcb327f4d --- /dev/null +++ b/archived/projt-launcher/launcher/launch/TaskBridgeStage.cpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "TaskBridgeStage.hpp" + +#include "tasks/Task.h" + +namespace projt::launch +{ + TaskBridgeStage::TaskBridgeStage(LaunchPipeline* pipeline, Task::Ptr task) + : LaunchStage(pipeline), + m_payload(std::move(task)) + {} + + void TaskBridgeStage::executeTask() + { + if (m_state == Task::State::AbortedByUser) + { + emitFailed(tr("Task aborted.")); + return; + } + connect(m_payload.get(), &Task::finished, this, &TaskBridgeStage::onPayloadFinished); + connect(m_payload.get(), &Task::progress, this, &TaskBridgeStage::setProgress); + connect(m_payload.get(), &Task::stepProgress, this, &TaskBridgeStage::propagateStepProgress); + connect(m_payload.get(), &Task::status, this, &TaskBridgeStage::setStatus); + connect(m_payload.get(), &Task::details, this, &TaskBridgeStage::setDetails); + emit progressReportingRequest(); + } + + void TaskBridgeStage::proceed() + { + if (m_payload) + { + m_payload->start(); + } + } + + void TaskBridgeStage::onPayloadFinished() + { + if (!m_payload) + { + emitFailed(tr("Task is missing.")); + return; + } + if (m_payload->wasSuccessful()) + { + m_payload.reset(); + emitSucceeded(); + } + else + { + QString reason = tr("Instance update failed because: %1\n\n").arg(m_payload->failReason()); + m_payload.reset(); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(reason); + } + } + + bool TaskBridgeStage::canAbort() const + { + if (m_payload) + { + return m_payload->canAbort(); + } + return true; + } + + bool TaskBridgeStage::abort() + { + if (m_payload && m_payload->canAbort()) + { + auto status = m_payload->abort(); + emitFailed("Aborted."); + return status; + } + return Task::abort(); + } +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/TaskBridgeStage.hpp b/archived/projt-launcher/launcher/launch/TaskBridgeStage.hpp new file mode 100644 index 0000000000..196168dfd1 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/TaskBridgeStage.hpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +namespace projt::launch +{ + class TaskBridgeStage : public LaunchStage + { + Q_OBJECT + public: + explicit TaskBridgeStage(LaunchPipeline* pipeline, Task::Ptr task); + ~TaskBridgeStage() override = default; + + void executeTask() override; + bool canAbort() const override; + void proceed() override; + + public slots: + bool abort() override; + + private slots: + void onPayloadFinished(); + + private: + Task::Ptr m_payload; + }; +} // namespace projt::launch diff --git a/archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.cpp b/archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.cpp new file mode 100644 index 0000000000..6a611c32f4 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "HostLookupReportStep.hpp" + +#include +#include + +namespace projt::launch::steps +{ + HostLookupReportStep::HostLookupReportStep(projt::launch::LaunchPipeline* parent, QStringList hosts) + : projt::launch::LaunchStage(parent), + m_hosts(hosts) + {} + + void HostLookupReportStep::executeTask() + { + if (m_hosts.isEmpty()) + { + emitSucceeded(); + return; + } + + m_pending = m_hosts.size(); + for (const auto& host : m_hosts) + { + int lookupId = QHostInfo::lookupHost(host, this, &HostLookupReportStep::onLookupFinished); + m_lookupById.insert(lookupId, host); + } + } + + void HostLookupReportStep::onLookupFinished(const QHostInfo& info) + { + if (isFinished()) + { + return; + } + + QString host = m_lookupById.value(info.lookupId(), info.hostName()); + m_messages.insert(host, formatMessage(host, info)); + m_pending--; + finalizeIfReady(); + } + + QString HostLookupReportStep::formatMessage(const QString& host, const QHostInfo& info) const + { + QStringList addresses; + for (const auto& address : info.addresses()) + { + addresses.append(address.toString()); + } + + QString addressText = addresses.isEmpty() ? "N/A" : addresses.join(", "); + QString errorSuffix; + if (info.error() != QHostInfo::NoError && !info.errorString().isEmpty()) + { + errorSuffix = QString(" (%1)").arg(info.errorString()); + } + + return QString("%1 resolves to:\n [%2]%3\n\n").arg(host, addressText, errorSuffix); + } + + void HostLookupReportStep::finalizeIfReady() + { + if (m_pending > 0) + { + return; + } + + for (const auto& host : m_hosts) + { + emit logLine(m_messages.value(host), MessageLevel::Launcher); + } + emitSucceeded(); + } + + bool HostLookupReportStep::canAbort() const + { + return true; + } +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.hpp b/archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.hpp new file mode 100644 index 0000000000..8b6cbef6c4 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/HostLookupReportStep.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include +#include + +class QHostInfo; + +namespace projt::launch::steps +{ + class HostLookupReportStep : public projt::launch::LaunchStage + { + Q_OBJECT + public: + HostLookupReportStep(projt::launch::LaunchPipeline* parent, QStringList hosts); + ~HostLookupReportStep() override = default; + + void executeTask() override; + bool canAbort() const override; + + private slots: + void onLookupFinished(const QHostInfo& info); + + private: + void finalizeIfReady(); + QString formatMessage(const QString& host, const QHostInfo& info) const; + + QHash m_lookupById; + QHash m_messages; + QStringList m_hosts; + int m_pending = 0; + }; +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.cpp b/archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.cpp new file mode 100644 index 0000000000..40dfd6022b --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LaunchCommandStep.hpp" + +#include + +#include + +namespace projt::launch::steps +{ + LaunchCommandStep::LaunchCommandStep(projt::launch::LaunchPipeline* parent, Hook hook, QString commandTemplate) + : projt::launch::LaunchStage(parent), + m_commandTemplate(commandTemplate), + m_hook(hook) + { + auto instance = m_flow->instance(); + m_runner.setProcessEnvironment(instance->createEnvironment()); + connect(&m_runner, &LoggedProcess::log, this, &LaunchCommandStep::logLines); + connect(&m_runner, &LoggedProcess::stateChanged, this, &LaunchCommandStep::onProcessState); + } + + void LaunchCommandStep::executeTask() + { + QString commandLine = m_flow->substituteVariables(m_commandTemplate).trimmed(); + if (commandLine.isEmpty()) + { + auto error = tr("%1 command is empty.").arg(hookLabel()); + emit logLine(error, MessageLevel::Error); + emitFailed(error); + return; + } + + emit logLine(tr("Running %1 command: %2").arg(hookLabel(), commandLine), MessageLevel::Launcher); + + auto parts = QProcess::splitCommand(commandLine); + if (parts.isEmpty()) + { + auto error = tr("%1 command is empty.").arg(hookLabel()); + emit logLine(error, MessageLevel::Error); + emitFailed(error); + return; + } + + QString program = parts.takeFirst(); + m_runner.start(program, parts); + } + + void LaunchCommandStep::onProcessState(LoggedProcess::State state) + { + switch (state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + auto error = buildFailure(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + return; + } + case LoggedProcess::Finished: + { + if (m_runner.exitCode() != 0) + { + auto error = buildFailure(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + } + else + { + emit logLine(tr("%1 command ran successfully.\n\n").arg(hookLabel()), MessageLevel::Launcher); + emitSucceeded(); + } + return; + } + default: break; + } + } + + void LaunchCommandStep::setWorkingDirectory(const QString& workDir) + { + m_runner.setWorkingDirectory(workDir); + } + + bool LaunchCommandStep::abort() + { + auto state = m_runner.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_runner.kill(); + } + return true; + } + + QString LaunchCommandStep::hookLabel() const + { + switch (m_hook) + { + case Hook::PreLaunch: return tr("Pre-Launch"); + case Hook::PostExit: return tr("Post-Launch"); + } + return tr("Launch"); + } + + QString LaunchCommandStep::buildFailure() const + { + return tr("%1 command failed with code %2.\n\n").arg(hookLabel()).arg(m_runner.exitCode()); + } +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.hpp b/archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.hpp new file mode 100644 index 0000000000..fef9e66a0c --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/LaunchCommandStep.hpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include + +namespace projt::launch::steps +{ + class LaunchCommandStep : public projt::launch::LaunchStage + { + Q_OBJECT + public: + enum class Hook + { + PreLaunch, + PostExit + }; + + LaunchCommandStep(projt::launch::LaunchPipeline* parent, Hook hook, QString commandTemplate); + ~LaunchCommandStep() override = default; + + void executeTask() override; + bool canAbort() const override + { + return true; + } + bool abort() override; + + void setWorkingDirectory(const QString& workDir); + + private slots: + void onProcessState(LoggedProcess::State state); + + private: + QString hookLabel() const; + QString buildFailure() const; + + LoggedProcess m_runner; + QString m_commandTemplate; + Hook m_hook; + }; +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/LogMessageStep.cpp b/archived/projt-launcher/launcher/launch/steps/LogMessageStep.cpp new file mode 100644 index 0000000000..180b1815be --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/LogMessageStep.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LogMessageStep.hpp" + +namespace projt::launch::steps +{ + LogMessageStep::LogMessageStep(projt::launch::LaunchPipeline* parent, QStringList lines, MessageLevel::Enum level) + : projt::launch::LaunchStage(parent), + m_lines(lines), + m_level(level) + {} + + LogMessageStep::LogMessageStep(projt::launch::LaunchPipeline* parent, const QString& line, MessageLevel::Enum level) + : projt::launch::LaunchStage(parent), + m_lines(QStringList{ line }), + m_level(level) + {} + + void LogMessageStep::executeTask() + { + emit logLines(m_lines, m_level); + emitSucceeded(); + } + + bool LogMessageStep::canAbort() const + { + return true; + } + + bool LogMessageStep::abort() + { + emitAborted(); + return true; + } +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/LogMessageStep.hpp b/archived/projt-launcher/launcher/launch/steps/LogMessageStep.hpp new file mode 100644 index 0000000000..5abed8aab3 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/LogMessageStep.hpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include + +namespace projt::launch::steps +{ + class LogMessageStep : public projt::launch::LaunchStage + { + Q_OBJECT + public: + explicit LogMessageStep(projt::launch::LaunchPipeline* parent, QStringList lines, MessageLevel::Enum level); + explicit LogMessageStep(projt::launch::LaunchPipeline* parent, const QString& line, MessageLevel::Enum level); + ~LogMessageStep() override = default; + + void executeTask() override; + bool canAbort() const override; + bool abort() override; + + private: + QStringList m_lines; + MessageLevel::Enum m_level; + }; +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.cpp b/archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.cpp new file mode 100644 index 0000000000..8c1c7e4fa1 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "QuitAfterGameStep.hpp" + +#include "Application.h" + +namespace projt::launch::steps +{ + QuitAfterGameStep::QuitAfterGameStep(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) + {} + + void QuitAfterGameStep::executeTask() + { + APPLICATION->quit(); + emitSucceeded(); + } +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.hpp b/archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.hpp new file mode 100644 index 0000000000..53a9093222 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/QuitAfterGameStep.hpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +namespace projt::launch::steps +{ + class QuitAfterGameStep : public projt::launch::LaunchStage + { + Q_OBJECT + public: + explicit QuitAfterGameStep(projt::launch::LaunchPipeline* parent); + ~QuitAfterGameStep() override = default; + + void executeTask() override; + bool canAbort() const override + { + return false; + } + }; +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.cpp b/archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.cpp new file mode 100644 index 0000000000..9c7902e1e0 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.cpp @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "RuntimeProbeStep.hpp" + +#include +#include + +#include +#include +#include + +namespace projt::launch::steps +{ + RuntimeProbeStep::RuntimeProbeStep(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) + {} + + bool RuntimeProbeStep::CachedRuntimeInfo::isComplete() const + { + return !version.isEmpty() && !arch.isEmpty() && !realArch.isEmpty() && !vendor.isEmpty(); + } + + RuntimeProbeStep::CachedRuntimeInfo RuntimeProbeStep::readCache() const + { + auto settings = m_flow->instance()->settings(); + CachedRuntimeInfo cache; + cache.signature = settings->get("JavaSignature").toString(); + cache.version = settings->get("JavaVersion").toString(); + cache.arch = settings->get("JavaArchitecture").toString(); + cache.realArch = settings->get("JavaRealArchitecture").toString(); + cache.vendor = settings->get("JavaVendor").toString(); + return cache; + } + + QString RuntimeProbeStep::computeSignature(const QFileInfo& info, const QString& resolvedPath) const + { + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(QByteArray::number(info.lastModified().toMSecsSinceEpoch())); + hash.addData(resolvedPath.toUtf8()); + return hash.result().toHex(); + } + + bool RuntimeProbeStep::needsProbe(const CachedRuntimeInfo& cache, const QString& signature) const + { + if (cache.signature != signature) + { + return true; + } + return !cache.isComplete(); + } + + void RuntimeProbeStep::executeTask() + { + auto instance = m_flow->instance(); + auto settings = instance->settings(); + + QString configuredPath = settings->get("JavaPath").toString(); + m_resolvedPath = FS::ResolveExecutable(configuredPath); + + bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); + + QString executablePath = QStandardPaths::findExecutable(m_resolvedPath); + if (executablePath.isEmpty()) + { + if (perInstance) + { + emit logLine( + QString("The Java binary \"%1\" couldn't be found. Please fix the Java path override in the " + "instance's settings or disable it.") + .arg(configuredPath), + MessageLevel::Warning); + } + else + { + emit logLine(QString("The Java binary \"%1\" couldn't be found. Please set up Java in the settings.") + .arg(configuredPath), + MessageLevel::Warning); + } + emitFailed(QString("Java path is not valid.")); + return; + } + + emit logLine("Java path is:\n" + m_resolvedPath + "\n\n", MessageLevel::Launcher); + + if (projt::java::RuntimeProbeTask::probeJarPath().isEmpty()) + { + const char* reason = QT_TR_NOOP("Java checker library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + + QFileInfo javaInfo(executablePath); + m_signature = computeSignature(javaInfo, m_resolvedPath); + + auto cache = readCache(); + if (!needsProbe(cache, m_signature)) + { + printRuntimeInfo(cache.version, cache.arch, cache.realArch, cache.vendor); + instance->updateRuntimeContext(); + emitSucceeded(); + return; + } + + projt::java::RuntimeProbeTask::ProbeSettings probeSettings; + probeSettings.binaryPath = executablePath; + m_probeTask.reset(new projt::java::RuntimeProbeTask(probeSettings)); + emit logLine(tr("Checking Java version..."), MessageLevel::Launcher); + connect(m_probeTask.get(), + &projt::java::RuntimeProbeTask::probeFinished, + this, + &RuntimeProbeStep::onProbeFinished); + m_probeTask->start(); + } + + void RuntimeProbeStep::onProbeFinished(const projt::java::RuntimeProbeTask::ProbeReport& report) + { + switch (report.status) + { + case projt::java::RuntimeProbeTask::ProbeReport::Status::Errored: + { + emit logLine(QString("Could not start java:"), MessageLevel::Error); + emit logLines(report.stderrLog.split('\n'), MessageLevel::Error); + emit logLine(QString("\nCheck your Java settings."), MessageLevel::Launcher); + emitFailed(QString("Could not start java!")); + return; + } + case projt::java::RuntimeProbeTask::ProbeReport::Status::InvalidData: + { + emit logLine(QString("Java checker returned some invalid data we don't understand:"), + MessageLevel::Error); + emit logLines(report.stdoutLog.split('\n'), MessageLevel::Warning); + emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); + m_flow->instance()->updateRuntimeContext(); + emitSucceeded(); + return; + } + case projt::java::RuntimeProbeTask::ProbeReport::Status::Valid: + { + printRuntimeInfo(report.version.toString(), report.platformTag, report.platformArch, report.vendor); + storeProbeResult(report); + m_flow->instance()->updateRuntimeContext(); + emitSucceeded(); + return; + } + } + } + + void RuntimeProbeStep::storeProbeResult(const projt::java::RuntimeProbeTask::ProbeReport& report) const + { + auto settings = m_flow->instance()->settings(); + settings->set("JavaVersion", report.version.toString()); + settings->set("JavaArchitecture", report.platformTag); + settings->set("JavaRealArchitecture", report.platformArch); + settings->set("JavaVendor", report.vendor); + settings->set("JavaSignature", m_signature); + } + + void RuntimeProbeStep::printRuntimeInfo(const QString& version, + const QString& architecture, + const QString& realArchitecture, + const QString& vendor) + { + emit logLine(QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n") + .arg(version, architecture, realArchitecture, vendor), + MessageLevel::Launcher); + } +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.hpp b/archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.hpp new file mode 100644 index 0000000000..9c1cd47407 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/RuntimeProbeStep.hpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include + +class QFileInfo; + +namespace projt::launch::steps +{ + class RuntimeProbeStep : public projt::launch::LaunchStage + { + Q_OBJECT + public: + explicit RuntimeProbeStep(projt::launch::LaunchPipeline* parent); + ~RuntimeProbeStep() override = default; + + void executeTask() override; + bool canAbort() const override + { + return false; + } + + private slots: + void onProbeFinished(const projt::java::RuntimeProbeTask::ProbeReport& report); + + private: + struct CachedRuntimeInfo + { + QString signature; + QString version; + QString arch; + QString realArch; + QString vendor; + + bool isComplete() const; + }; + + CachedRuntimeInfo readCache() const; + QString computeSignature(const QFileInfo& info, const QString& resolvedPath) const; + bool needsProbe(const CachedRuntimeInfo& cache, const QString& signature) const; + void storeProbeResult(const projt::java::RuntimeProbeTask::ProbeReport& report) const; + void printRuntimeInfo(const QString& version, + const QString& architecture, + const QString& realArchitecture, + const QString& vendor); + + QString m_resolvedPath; + QString m_signature; + projt::java::RuntimeProbeTask::Ptr m_probeTask; + }; +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.cpp b/archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.cpp new file mode 100644 index 0000000000..4b23e9a4d5 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "ServerJoinResolveStep.hpp" + +#include + +#include + +namespace projt::launch::steps +{ + ServerJoinResolveStep::ServerJoinResolveStep(projt::launch::LaunchPipeline* parent) + : projt::launch::LaunchStage(parent) + { + auto lookup = new QDnsLookup(this); + lookup->setType(QDnsLookup::SRV); + connect(lookup, &QDnsLookup::finished, this, &ServerJoinResolveStep::onLookupFinished); + m_lookup = lookup; + } + + void ServerJoinResolveStep::setLookupAddress(const QString& address) + { + m_lookupAddress = address; + m_queryName = QString("_minecraft._tcp.%1").arg(address); + if (m_lookup) + { + m_lookup->setName(m_queryName); + } + } + + void ServerJoinResolveStep::setOutputTarget(MinecraftTarget::Ptr target) + { + m_output = target; + } + + void ServerJoinResolveStep::executeTask() + { + if (!m_lookup) + { + emitFailed(QString("DNS lookup failed to initialize.")); + return; + } + if (m_lookupAddress.isEmpty()) + { + emitFailed(QString("Server address is empty.")); + return; + } + if (!m_output) + { + emitFailed(QString("Server target is missing.")); + return; + } + m_lookup->lookup(); + } + + bool ServerJoinResolveStep::abort() + { + if (m_lookup) + { + m_lookup->abort(); + } + emitAborted(); + return true; + } + + void ServerJoinResolveStep::onLookupFinished() + { + if (isFinished()) + { + return; + } + if (!m_lookup) + { + emitFailed(QString("DNS lookup failed to initialize.")); + return; + } + + if (m_lookup->error() != QDnsLookup::NoError) + { + emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n") + .arg(m_lookup->name(), m_lookup->errorString()), + MessageLevel::Launcher); + finalizeTarget(m_lookupAddress, 25565); + return; + } + + const auto records = m_lookup->serviceRecords(); + if (records.empty()) + { + emit logLine( + QString( + "Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n") + .arg(m_lookup->name()), + MessageLevel::Warning); + finalizeTarget(m_lookupAddress, 25565); + return; + } + + const auto& firstRecord = records.at(0); + quint16 port = firstRecord.port(); + + emit logLine(QString("Resolved server address %1 to %2 with port %3\n") + .arg(m_lookup->name(), firstRecord.target(), QString::number(port)), + MessageLevel::Launcher); + finalizeTarget(firstRecord.target(), port); + } + + void ServerJoinResolveStep::finalizeTarget(const QString& address, quint16 port) + { + m_output->address = address; + m_output->port = port; + + emitSucceeded(); + } +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.hpp b/archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.hpp new file mode 100644 index 0000000000..c4f306b5a4 --- /dev/null +++ b/archived/projt-launcher/launcher/launch/steps/ServerJoinResolveStep.hpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include +#include + +class QDnsLookup; + +namespace projt::launch::steps +{ + class ServerJoinResolveStep : public projt::launch::LaunchStage + { + Q_OBJECT + public: + explicit ServerJoinResolveStep(projt::launch::LaunchPipeline* parent); + ~ServerJoinResolveStep() override = default; + + void executeTask() override; + bool abort() override; + bool canAbort() const override + { + return true; + } + + void setLookupAddress(const QString& address); + void setOutputTarget(MinecraftTarget::Ptr target); + + private slots: + void onLookupFinished(); + + private: + void finalizeTarget(const QString& address, quint16 port); + + QPointer m_lookup; + QString m_lookupAddress; + QString m_queryName; + MinecraftTarget::Ptr m_output; + }; +} // namespace projt::launch::steps diff --git a/archived/projt-launcher/launcher/logs/LogEventParser.cpp b/archived/projt-launcher/launcher/logs/LogEventParser.cpp new file mode 100644 index 0000000000..15b4ee61f5 --- /dev/null +++ b/archived/projt-launcher/launcher/logs/LogEventParser.cpp @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LogEventParser.hpp" + +#include + +using namespace Qt::Literals::StringLiterals; + +namespace projt::logs +{ + void LogEventParser::pushLine(QAnyStringView data) + { + // If we have holdover data from incomplete XML, prepend it + if (!m_holdover.isEmpty()) + { + m_buffer = m_holdover; + m_buffer.append('\n'); + m_holdover.clear(); + } + // Otherwise, clear buffer before adding new data to prevent duplicates + else if (!m_buffer.isEmpty()) + { + m_buffer.clear(); + } + m_buffer.append(data.toString()); + } + + std::optional LogEventParser::lastError() const + { + return m_error; + } + + void LogEventParser::captureError() + { + m_error = { + m_reader.errorString(), + m_reader.error(), + }; + } + + void LogEventParser::clearError() + { + m_error = {}; + } + + bool LogEventParser::isPotentialLog4j(QStringView view) const + { + static const QString marker = QStringLiteral(" LogEventParser::readEventAttributes() + { + LogRecord entry; + entry.level = MessageLevel::Info; + auto attributes = m_reader.attributes(); + + for (const auto& attr : attributes) + { + const auto name = attr.name(); + const auto value = attr.value(); + if (name == "logger"_L1) + { + entry.logger = value.trimmed().toString(); + } + else if (name == "timestamp"_L1) + { + if (value.trimmed().isEmpty()) + { + m_reader.raiseError("log4j:Event missing timestamp attribute"); + return {}; + } + entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); + } + else if (name == "level"_L1) + { + entry.levelText = value.trimmed().toString(); + entry.level = MessageLevel::getLevel(entry.levelText); + } + else if (name == "thread"_L1) + { + entry.thread = value.trimmed().toString(); + } + } + + if (entry.logger.isEmpty()) + { + m_reader.raiseError("log4j:Event missing logger attribute"); + return {}; + } + + return entry; + } + + std::optional LogEventParser::parseLog4jEvent() + { + m_reader.clear(); + m_reader.setNamespaceProcessing(false); + m_reader.addData(m_buffer); + + m_reader.readNextStartElement(); + if (m_reader.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) != 0) + { + return {}; + } + + auto entry = readEventAttributes(); + if (!entry.has_value()) + { + captureError(); + return {}; + } + + bool haveMessage = false; + while (!m_reader.atEnd()) + { + auto token = m_reader.readNext(); + if (token == QXmlStreamReader::TokenType::StartElement) + { + if (m_reader.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) + { + entry->message = m_reader.readElementText(QXmlStreamReader::IncludeChildElements); + if (entry->message.endsWith(QLatin1Char('\n'))) + { + entry->message.chop(1); + } + haveMessage = true; + } + } + else if (token == QXmlStreamReader::TokenType::EndElement) + { + if (m_reader.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) + { + if (!haveMessage) + { + m_reader.raiseError("log4j:Event missing message element"); + captureError(); + return {}; + } + auto consumed = m_reader.characterOffset(); + if (consumed > 0 && consumed <= m_buffer.length()) + { + m_buffer = m_buffer.right(m_buffer.length() - consumed); + } + clearError(); + return entry.value(); + } + } + else if (token == QXmlStreamReader::TokenType::EndDocument) + { + return {}; + } + + if (m_reader.hasError()) + { + captureError(); + return {}; + } + } + + return {}; + } + + std::optional LogEventParser::popNext() + { + clearError(); + + if (m_buffer.isEmpty()) + { + return {}; + } + + // Ignore leading whitespace before a log4j XML event. + { + qsizetype firstNonSpace = -1; + for (qsizetype i = 0; i < m_buffer.size(); ++i) + { + if (!m_buffer.at(i).isSpace()) + { + firstNonSpace = i; + break; + } + } + if (firstNonSpace > 0) + { + const QStringView slice = QStringView{ m_buffer }.sliced(firstNonSpace); + if (!slice.isEmpty() && slice.front() == QLatin1Char('<') && isPotentialLog4j(slice)) + { + m_buffer = m_buffer.mid(firstNonSpace); + } + } + } + + if (m_buffer.trimmed().isEmpty()) + { + auto text = QString(m_buffer); + m_buffer.clear(); + return RawLine{ text }; + } + + if (isPotentialLog4j(QStringView(m_buffer))) + { + if (!looksCompleteLog4j()) + { + m_holdover = m_buffer; + return PendingChunk{ m_buffer }; + } + return parseLog4jEvent(); + } + + const QStringView view(m_buffer); + qsizetype offset = 0; + while (offset < view.length()) + { + auto rel = view.sliced(offset).indexOf(QLatin1Char('<')); + if (rel < 0) + { + break; + } + auto pos = offset + rel; + auto slice = view.sliced(pos); + if (isPotentialLog4j(slice)) + { + if (pos > 0) + { + auto text = m_buffer.left(pos); + m_buffer = m_buffer.right(m_buffer.length() - pos); + return RawLine{ text }; + } + m_holdover = m_buffer; + return PendingChunk{ m_buffer }; + } + offset = pos + 1; + } + + auto text = QString(m_buffer); + m_buffer.clear(); + return RawLine{ text }; + } + + QList LogEventParser::drainAvailable() + { + QList items; + bool keepGoing = true; + while (keepGoing) + { + auto item = popNext(); + if (m_error.has_value()) + { + return {}; + } + if (item.has_value()) + { + if (std::holds_alternative(item.value())) + { + break; + } + items.push_back(item.value()); + } + else + { + keepGoing = false; + } + } + return items; + } + + MessageLevel::Enum LogEventParser::guessLevelFromLine(const QString& line, MessageLevel::Enum fallback) + { + static const QRegularExpression kLogHeader("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + auto match = kLogHeader.match(line); + if (match.hasMatch()) + { + auto levelText = match.captured("level"); + fallback = MessageLevel::getLevel(levelText); + } + else + { + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") + || line.contains("[FINER]") || line.contains("[FINEST]")) + { + fallback = MessageLevel::Info; + } + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + { + fallback = MessageLevel::Error; + } + if (line.contains("[WARNING]")) + { + fallback = MessageLevel::Warning; + } + if (line.contains("[DEBUG]")) + { + fallback = MessageLevel::Debug; + } + } + if (fallback != MessageLevel::Unknown) + { + return fallback; + } + if (line.contains("overwriting existing")) + { + return MessageLevel::Fatal; + } + return MessageLevel::Info; + } +} // namespace projt::logs diff --git a/archived/projt-launcher/launcher/logs/LogEventParser.hpp b/archived/projt-launcher/launcher/logs/LogEventParser.hpp new file mode 100644 index 0000000000..a3aff8fb5e --- /dev/null +++ b/archived/projt-launcher/launcher/logs/LogEventParser.hpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MessageLevel.h" + +namespace projt::logs +{ + class LogEventParser + { + public: + struct LogRecord + { + QString logger; + MessageLevel::Enum level = MessageLevel::Unknown; + QString levelText; + QDateTime timestamp; + QString thread; + QString message; + }; + + struct RawLine + { + QString text; + }; + + struct PendingChunk + { + QString data; + }; + + struct ParseFailure + { + QString message; + QXmlStreamReader::Error code = QXmlStreamReader::Error::NoError; + }; + + using Item = std::variant; + + LogEventParser() = default; + + void pushLine(QAnyStringView data); + std::optional popNext(); + QList drainAvailable(); + std::optional lastError() const; + + static MessageLevel::Enum guessLevelFromLine(const QString& line, MessageLevel::Enum fallback); + + private: + std::optional readEventAttributes(); + std::optional parseLog4jEvent(); + bool isPotentialLog4j(QStringView view) const; + bool looksCompleteLog4j() const; + void captureError(); + void clearError(); + + QString m_buffer; + QString m_holdover; + QXmlStreamReader m_reader; + std::optional m_error; + }; +} // namespace projt::logs diff --git a/archived/projt-launcher/launcher/logs/LogRedactor.cpp b/archived/projt-launcher/launcher/logs/LogRedactor.cpp new file mode 100644 index 0000000000..15e16d2521 --- /dev/null +++ b/archived/projt-launcher/launcher/logs/LogRedactor.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LogRedactor.hpp" + +#include +#include + +namespace projt::logs +{ + struct RedactionRule + { + QRegularExpression pattern; + QString replacement; + }; + + static QVector buildDefaultRules() + { + QVector rules; + rules.reserve(7); + rules.push_back( + { QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption), + "C:\\Users\\********\\" }); + rules.push_back({ QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption), + "C:/Users/********/" }); + rules.push_back( + { QRegularExpression("(?)" }); + rules.push_back( + { QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "new refresh token: \"\"" }); + rules.push_back( + { QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "\"device_code\" : \"\"" }); + + for (auto& rule : rules) + { + rule.pattern.optimize(); + } + return rules; + } + + static const QVector& ruleset() + { + static const QVector rules = buildDefaultRules(); + return rules; + } + + void redactLog(QString& text) + { + for (const auto& rule : ruleset()) + { + text.replace(rule.pattern, rule.replacement); + } + } + + QString redactCopy(const QString& text) + { + QString output = text; + redactLog(output); + return output; + } +} // namespace projt::logs diff --git a/archived/projt-launcher/launcher/logs/LogRedactor.hpp b/archived/projt-launcher/launcher/logs/LogRedactor.hpp new file mode 100644 index 0000000000..72e9033cf9 --- /dev/null +++ b/archived/projt-launcher/launcher/logs/LogRedactor.hpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +namespace projt::logs +{ + void redactLog(QString& text); + QString redactCopy(const QString& text); +} // namespace projt::logs diff --git a/archived/projt-launcher/launcher/main.cpp b/archived/projt-launcher/launcher/main.cpp new file mode 100644 index 0000000000..434e93edcc --- /dev/null +++ b/archived/projt-launcher/launcher/main.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/* === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ======================================================================== */ + +#include "Application.h" + +#if defined(Q_OS_LINUX) +#include "CefRuntime.h" + +#include +#endif + +int main(int argc, char* argv[]) +{ +#if defined(Q_OS_LINUX) + // Prefer GTK-themed integration on GNOME if user hasn't set a platform theme. + if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORMTHEME")) + { + const auto desktop = qgetenv("XDG_CURRENT_DESKTOP").toLower(); + if (desktop.contains("gnome")) + { + qputenv("QT_QPA_PLATFORMTHEME", "gnome"); + } + } + + const QByteArray sessionType = qgetenv("XDG_SESSION_TYPE").toLower(); + const QByteArray qtPlatform = qgetenv("QT_QPA_PLATFORM").toLower(); + const bool nativeWaylandRequested = (sessionType == "wayland" || !qgetenv("WAYLAND_DISPLAY").isEmpty()) + && (qtPlatform.isEmpty() || qtPlatform.startsWith("wayland")); + if (nativeWaylandRequested) + { + qputenv("QT_QPA_PLATFORM", "xcb"); + } + +#if defined(PROJT_USE_CEF) + const int cefExitCode = projt::cef::Runtime::executeSecondaryProcess(argc, argv); + if (cefExitCode >= 0) + { + return cefExitCode; + } +#endif +#endif + + // initialize Qt + Application app(argc, argv); + + switch (app.status()) + { + case Application::StartingUp: + case Application::Initialized: + { +#if defined(Q_OS_LINUX) && defined(PROJT_USE_CEF) + if (!projt::cef::Runtime::instance().initializeBrowserProcess(argc, argv)) + { + return projt::cef::Runtime::instance().exitCode(); + } +#endif + + Q_INIT_RESOURCE(multimc); + Q_INIT_RESOURCE(backgrounds); + Q_INIT_RESOURCE(documents); + Q_INIT_RESOURCE(projtlauncher); + + Q_INIT_RESOURCE(pe_dark); + Q_INIT_RESOURCE(pe_light); + Q_INIT_RESOURCE(pe_blue); + Q_INIT_RESOURCE(pe_colored); + Q_INIT_RESOURCE(breeze_dark); + Q_INIT_RESOURCE(breeze_light); + Q_INIT_RESOURCE(OSX); + Q_INIT_RESOURCE(iOS); + Q_INIT_RESOURCE(flat); + Q_INIT_RESOURCE(flat_white); + + Q_INIT_RESOURCE(shaders); + const int exitCode = app.exec(); +#if defined(Q_OS_LINUX) && defined(PROJT_USE_CEF) + projt::cef::Runtime::instance().shutdown(); +#endif + return exitCode; + } + case Application::Failed: return 1; + case Application::Succeeded: return 0; + default: return -1; + } +} diff --git a/archived/projt-launcher/launcher/meta/BaseEntity.cpp b/archived/projt-launcher/launcher/meta/BaseEntity.cpp new file mode 100644 index 0000000000..785653a6e4 --- /dev/null +++ b/archived/projt-launcher/launcher/meta/BaseEntity.cpp @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "BaseEntity.hpp" + +#include +#include + +#include "Application.h" +#include "BuildConfig.h" +#include "Exception.h" +#include "FileSystem.h" +#include "Json.h" +#include "modplatform/helpers/HashUtils.h" +#include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" +#include "net/HttpMetaCache.h" + +namespace projt::meta +{ + + namespace + { + + /** + * @brief Validator that parses downloaded JSON into the target entity. + */ + class JsonParseValidator : public Net::Validator + { + public: + explicit JsonParseValidator(MetaEntity* entity) : m_entity(entity) + {} + + bool init(QNetworkRequest&) override + { + m_buffer.clear(); + return true; + } + + bool write(QByteArray& chunk) override + { + m_buffer.append(chunk); + return true; + } + + bool abort() override + { + m_buffer.clear(); + return true; + } + + bool validate(QNetworkReply&) override + { + QString filename = m_entity->cacheFilePath(); + try + { + QJsonDocument doc = Json::requireDocument(m_buffer, filename); + QJsonObject root = Json::requireObject(doc, filename); + m_entity->loadFromJson(root); + return true; + } + catch (const Exception& ex) + { + qWarning() << "Failed to parse metadata:" << ex.cause(); + return false; + } + } + + private: + QByteArray m_buffer; + MetaEntity* m_entity; + }; + + } // anonymous namespace + + QUrl MetaEntity::remoteUrl() const + { + auto settings = APPLICATION->settings(); + QString override = settings->get("MetaURLOverride").toString(); + + QString baseUrl = override.isEmpty() ? BuildConfig.META_URL : override; + return QUrl(baseUrl).resolved(cacheFilePath()); + } + + Task::Ptr MetaEntity::createLoadTask(Net::Mode mode) + { + if (m_activeTask && m_activeTask->isRunning()) + return m_activeTask; + + m_activeTask = Task::Ptr(new EntityLoader(this, mode)); + return m_activeTask; + } + + // EntityLoader implementation + + EntityLoader::EntityLoader(MetaEntity* target, Net::Mode mode) : m_target(target), m_mode(mode) + {} + + void EntityLoader::executeTask() + { + attemptLocalLoad(); + } + + void EntityLoader::attemptLocalLoad() + { + QString cachePath = QDir("meta").absoluteFilePath(m_target->cacheFilePath()); + + if (!QFile::exists(cachePath)) + { + // No local cache, need remote fetch + if (m_mode == Net::Mode::Offline) + { + emitFailed(tr("Metadata not available offline: %1").arg(m_target->cacheFilePath())); + return; + } + initiateRemoteFetch(); + return; + } + + QString cacheError; + + if (m_mode == Net::Mode::Online) + { + // In online mode, always revalidate metadata against the remote cache entry on first load. + // Relying only on the cached file plus the last known expected checksum can leave us stuck + // on stale package manifests across launcher restarts. + if (loadCachedMetadata(cachePath, &cacheError)) + { + m_target->m_state = MetaEntity::State::Cached; + m_canUseLocalFallback = true; + } + else + { + qDebug() << "Cache parse failed for" << cachePath << ":" << cacheError; + FS::deletePath(cachePath); + m_target->m_state = MetaEntity::State::Pending; + m_canUseLocalFallback = false; + } + initiateRemoteFetch(); + return; + } + + if (loadCachedMetadata(cachePath, &cacheError)) + { + m_target->m_state = MetaEntity::State::Cached; + emitSucceeded(); + return; + } + + qDebug() << "Cache parse failed for" << cachePath << ":" << cacheError; + FS::deletePath(cachePath); + m_target->m_state = MetaEntity::State::Pending; + + if (m_mode == Net::Mode::Offline) + { + emitFailed(tr("Cached metadata corrupted and offline mode active")); + return; + } + + initiateRemoteFetch(); + } + + void EntityLoader::initiateRemoteFetch() + { + setStatus(tr("Downloading metadata: %1").arg(m_target->cacheFilePath())); + + m_netTask = NetJob::Ptr(new NetJob(tr("Fetch %1").arg(m_target->cacheFilePath()), APPLICATION->network())); + + auto cacheEntry = APPLICATION->metacache()->resolveEntry("meta", m_target->cacheFilePath()); + cacheEntry->setStale(true); + + auto download = Net::ApiDownload::makeCached(m_target->remoteUrl(), cacheEntry); + + // Add checksum validator if available + if (!m_target->m_expectedSha256.isEmpty()) + { + download->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha256, m_target->m_expectedSha256)); + } + + // Add JSON parsing validator + download->addValidator(new JsonParseValidator(m_target)); + + m_netTask->addNetAction(download); + m_netTask->setAskRetry(false); + + // Connect signals + connect(m_netTask.get(), &Task::succeeded, this, [this]() { finalizeLoad(MetaEntity::State::Synchronized); }); + connect(m_netTask.get(), &Task::failed, this, &EntityLoader::handleRemoteFailure); + connect(m_netTask.get(), &Task::progress, this, &Task::setProgress); + connect(m_netTask.get(), &Task::stepProgress, this, &EntityLoader::propagateStepProgress); + connect(m_netTask.get(), &Task::status, this, &Task::setStatus); + connect(m_netTask.get(), &Task::details, this, &Task::setDetails); + + m_netTask->start(); + } + + bool EntityLoader::loadCachedMetadata(const QString& cachePath, QString* errorOut) + { + try + { + setStatus(tr("Loading cached metadata")); + + QByteArray content = FS::read(cachePath); + m_target->m_actualSha256 = Hashing::hash(content, Hashing::Algorithm::Sha256); + + if (m_target->m_state == MetaEntity::State::Pending) + { + QJsonDocument doc = Json::requireDocument(content, cachePath); + QJsonObject root = Json::requireObject(doc, cachePath); + m_target->loadFromJson(root); + } + + return true; + } + catch (const Exception& ex) + { + if (errorOut) + *errorOut = ex.cause(); + return false; + } + } + + void EntityLoader::handleRemoteFailure(const QString& reason) + { + if (!m_canUseLocalFallback) + { + emitFailed(reason); + return; + } + + QString cachePath = QDir("meta").absoluteFilePath(m_target->cacheFilePath()); + QString cacheError; + if (QFile::exists(cachePath) && loadCachedMetadata(cachePath, &cacheError)) + { + m_target->m_state = MetaEntity::State::Cached; + emitSucceeded(); + qWarning() << "Remote metadata refresh failed for" << m_target->cacheFilePath() + << "- falling back to cached metadata:" << reason; + return; + } + + if (!cacheError.isEmpty()) + qWarning() << "Cached fallback also failed for" << cachePath << ":" << cacheError; + + emitFailed(reason); + } + + void EntityLoader::finalizeLoad(MetaEntity::State newState) + { + m_target->m_state = newState; + if (newState == MetaEntity::State::Synchronized) + { + m_target->m_actualSha256 = m_target->m_expectedSha256; + } + emitSucceeded(); + } + + bool EntityLoader::canAbort() const + { + return m_netTask ? m_netTask->canAbort() : false; + } + + bool EntityLoader::abort() + { + if (m_netTask) + { + Task::abort(); + return m_netTask->abort(); + } + return Task::abort(); + } + +} // namespace projt::meta diff --git a/archived/projt-launcher/launcher/meta/BaseEntity.hpp b/archived/projt-launcher/launcher/meta/BaseEntity.hpp new file mode 100644 index 0000000000..69ce49d4bf --- /dev/null +++ b/archived/projt-launcher/launcher/meta/BaseEntity.hpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "net/Mode.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +namespace projt::meta +{ + + class EntityLoader; + + /** + * @brief Cacheable metadata entity loaded from local cache or remote. + */ + class MetaEntity + { + friend class EntityLoader; + + public: + using Ptr = std::shared_ptr; + + enum class State + { + Pending, + Cached, + Synchronized + }; + + virtual ~MetaEntity() = default; + + virtual QString cacheFilePath() const = 0; + virtual QUrl remoteUrl() const; + + State state() const + { + return m_state; + } + bool isReady() const + { + return m_state != State::Pending; + } + + void setExpectedChecksum(const QString& sha256) + { + m_expectedSha256 = sha256; + } + QString expectedChecksum() const + { + return m_expectedSha256; + } + + virtual void loadFromJson(const QJsonObject& json) = 0; + + [[nodiscard]] Task::Ptr createLoadTask(Net::Mode mode = Net::Mode::Online); + + protected: + State m_state = State::Pending; + QString m_expectedSha256; + QString m_actualSha256; + Task::Ptr m_activeTask; + }; + + /** + * @brief Task for loading a MetaEntity. + */ + class EntityLoader : public Task + { + Q_OBJECT + + public: + explicit EntityLoader(MetaEntity* target, Net::Mode mode); + ~EntityLoader() override = default; + + protected: + void executeTask() override; + bool canAbort() const override; + bool abort() override; + + private: + void attemptLocalLoad(); + void initiateRemoteFetch(); + bool loadCachedMetadata(const QString& cachePath, QString* errorOut = nullptr); + void handleRemoteFailure(const QString& reason); + void finalizeLoad(MetaEntity::State newState); + + MetaEntity* m_target; + Net::Mode m_mode; + NetJob::Ptr m_netTask; + bool m_canUseLocalFallback = false; + }; + +} // namespace projt::meta diff --git a/archived/projt-launcher/launcher/meta/Index.cpp b/archived/projt-launcher/launcher/meta/Index.cpp new file mode 100644 index 0000000000..bb7ca5aa11 --- /dev/null +++ b/archived/projt-launcher/launcher/meta/Index.cpp @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "Index.hpp" + +#include + +#include "JsonFormat.hpp" +#include "QObjectPtr.h" +#include "tasks/SequentialTask.h" + +namespace projt::meta +{ + + MetaIndex::MetaIndex(QObject* parent) : QAbstractListModel(parent) + {} + + MetaIndex::MetaIndex(const QList& components, QObject* parent) + : QAbstractListModel(parent), + m_components(components) + { + for (int i = 0; i < m_components.size(); ++i) + { + const auto& comp = m_components.at(i); + m_componentIndex.insert(comp->uid(), comp); + bindComponentSignals(i, comp); + } + } + + QVariant MetaIndex::data(const QModelIndex& idx, int role) const + { + if (!idx.isValid() || idx.row() < 0 || idx.row() >= m_components.size()) + return QVariant(); + + const MetaVersionList::Ptr& comp = m_components.at(idx.row()); + + switch (role) + { + case Qt::DisplayRole: return (idx.column() == 0) ? comp->displayName() : QVariant(); + case ComponentUidRole: return comp->uid(); + case ComponentNameRole: return comp->displayName(); + case ComponentListRole: return QVariant::fromValue(comp); + default: return QVariant(); + } + } + + int MetaIndex::rowCount(const QModelIndex& parent) const + { + return parent.isValid() ? 0 : m_components.size(); + } + + int MetaIndex::columnCount(const QModelIndex& parent) const + { + return parent.isValid() ? 0 : 1; + } + + QVariant MetaIndex::headerData(int section, Qt::Orientation orientation, int role) const + { + if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) + return tr("Component"); + return QVariant(); + } + + void MetaIndex::loadFromJson(const QJsonObject& json) + { + loadIndexFromJson(json, this); + } + + bool MetaIndex::hasComponent(const QString& uid) const + { + return m_componentIndex.contains(uid); + } + + MetaVersionList::Ptr MetaIndex::component(const QString& uid) + { + MetaVersionList::Ptr existing = m_componentIndex.value(uid); + if (existing) + return existing; + + auto newList = std::make_shared(uid); + int row = m_components.size(); + beginInsertRows(QModelIndex(), row, row); + m_componentIndex.insert(uid, newList); + m_components.append(newList); + bindComponentSignals(row, newList); + endInsertRows(); + + return newList; + } + + MetaVersion::Ptr MetaIndex::version(const QString& componentUid, const QString& versionId) + { + return component(componentUid)->getOrCreateVersion(versionId); + } + + void MetaIndex::mergeWith(const std::shared_ptr& other) + { + const auto& incoming = other->m_components; + + if (m_components.isEmpty()) + { + beginResetModel(); + m_components = incoming; + + for (int i = 0; i < incoming.size(); ++i) + { + const auto& comp = incoming.at(i); + m_componentIndex.insert(comp->uid(), comp); + bindComponentSignals(i, comp); + } + + endResetModel(); + return; + } + + for (const auto& incomingComp : incoming) + { + MetaVersionList::Ptr existing = m_componentIndex.value(incomingComp->uid()); + + if (existing) + { + existing->updateFromIndex(incomingComp); + } + else + { + int row = m_components.size(); + beginInsertRows(QModelIndex(), row, row); + + bindComponentSignals(row, incomingComp); + m_components.append(incomingComp); + m_componentIndex.insert(incomingComp->uid(), incomingComp); + + endInsertRows(); + } + } + } + + void MetaIndex::bindComponentSignals(int row, const MetaVersionList::Ptr& comp) + { + connect(comp.get(), + &MetaVersionList::displayNameChanged, + this, + [this, row]() { emit dataChanged(index(row), index(row), { Qt::DisplayRole, ComponentNameRole }); }); + } + + Task::Ptr MetaIndex::loadVersionTask(const QString& componentUid, + const QString& versionId, + Net::Mode mode, + bool forceRefresh) + { + if (mode == Net::Mode::Offline) + return version(componentUid, versionId)->createLoadTask(mode); + + auto compList = component(componentUid); + auto seq = makeShared(tr("Load metadata for %1:%2").arg(componentUid, versionId)); + + if (state() != MetaEntity::State::Synchronized || forceRefresh) + seq->addTask(this->createLoadTask(mode)); + + seq->addTask(compList->createLoadTask(mode)); + seq->addTask(compList->getOrCreateVersion(versionId)->createLoadTask(mode)); + + return seq; + } + + MetaVersion::Ptr MetaIndex::loadVersionBlocking(const QString& componentUid, const QString& versionId) + { + QEventLoop loop; + auto task = loadVersionTask(componentUid, versionId); + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); + task->start(); + loop.exec(); + + return version(componentUid, versionId); + } + +} // namespace projt::meta diff --git a/archived/projt-launcher/launcher/meta/Index.hpp b/archived/projt-launcher/launcher/meta/Index.hpp new file mode 100644 index 0000000000..6e672d6f76 --- /dev/null +++ b/archived/projt-launcher/launcher/meta/Index.hpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "BaseEntity.hpp" +#include "VersionList.hpp" +#include "net/Mode.h" + +class Task; + +namespace projt::meta +{ + + /** + * @brief Root index of all available metadata components. + */ + class MetaIndex : public QAbstractListModel, public MetaEntity + { + Q_OBJECT + + public: + explicit MetaIndex(QObject* parent = nullptr); + explicit MetaIndex(const QList& components, QObject* parent = nullptr); + ~MetaIndex() override = default; + + enum + { + ComponentUidRole = Qt::UserRole, + ComponentNameRole, + ComponentListRole + }; + + // QAbstractListModel interface + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent = {}) const override; + int columnCount(const QModelIndex& parent = {}) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + // MetaEntity interface + QString cacheFilePath() const override + { + return QStringLiteral("index.json"); + } + void loadFromJson(const QJsonObject& json) override; + + // Component access + bool hasComponent(const QString& uid) const; + MetaVersionList::Ptr component(const QString& uid); + MetaVersion::Ptr version(const QString& componentUid, const QString& versionId); + QList allComponents() const + { + return m_components; + } + + // Load operations + Task::Ptr loadVersionTask(const QString& componentUid, + const QString& versionId = {}, + Net::Mode mode = Net::Mode::Online, + bool forceRefresh = false); + + // Blocking load + MetaVersion::Ptr loadVersionBlocking(const QString& componentUid, const QString& versionId); + + public: // For parsers + void mergeWith(const std::shared_ptr& other); + + private: + void bindComponentSignals(int row, const MetaVersionList::Ptr& component); + + QList m_components; + QHash m_componentIndex; + }; + +} // namespace projt::meta diff --git a/archived/projt-launcher/launcher/meta/JsonFormat.cpp b/archived/projt-launcher/launcher/meta/JsonFormat.cpp new file mode 100644 index 0000000000..8c2d27102d --- /dev/null +++ b/archived/projt-launcher/launcher/meta/JsonFormat.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "JsonFormat.hpp" + +#include +#include + +#include "Index.hpp" +#include "Json.h" +#include "Version.hpp" +#include "VersionList.hpp" +#include "minecraft/OneSixVersionFormat.h" + +namespace projt::meta +{ + + SchemaVersion currentSchema() + { + return SchemaVersion::V1; + } + + SchemaVersion detectSchemaVersion(const QJsonObject& root, bool required) + { + if (!root.contains("formatVersion")) + return required ? SchemaVersion::Unknown : SchemaVersion::V1; + + QJsonValue val = root.value("formatVersion"); + if (!val.isDouble()) + return SchemaVersion::Unknown; + + int ver = val.toInt(); + return (ver == 0 || ver == 1) ? SchemaVersion::V1 : SchemaVersion::Unknown; + } + + void writeSchemaVersion(QJsonObject& target, SchemaVersion version) + { + if (version != SchemaVersion::Unknown) + target.insert("formatVersion", static_cast(version)); + } + + DependencySet parseDependencies(const QJsonObject& source, const QString& fieldName) + { + DependencySet result; + + if (!source.contains(fieldName)) + return result; + + QJsonArray arr = Json::requireArray(source, fieldName); + for (const QJsonValue& item : arr) + { + QJsonObject depObj = Json::requireObject(item); + + ComponentDependency dep; + dep.uid = Json::requireString(depObj, "uid"); + dep.equalsVersion = Json::ensureString(depObj, "equals", QString()); + dep.suggests = Json::ensureString(depObj, "suggests", QString()); + + result.insert(dep); + } + + return result; + } + + void writeDependencies(QJsonObject& target, const DependencySet& deps, const QString& fieldName) + { + if (deps.empty()) + return; + + QJsonArray arr; + for (const auto& dep : deps) + { + QJsonObject obj; + obj.insert("uid", dep.uid); + + if (!dep.equalsVersion.isEmpty()) + obj.insert("equals", dep.equalsVersion); + + if (!dep.suggests.isEmpty()) + obj.insert("suggests", dep.suggests); + + arr.append(obj); + } + + target.insert(fieldName, arr); + } + + namespace + { + + MetaVersionList::Ptr buildVersionListFromPackage(const QJsonObject& pkg) + { + QString uid = Json::requireString(pkg, "uid"); + auto list = std::make_shared(uid); + + list->setDisplayName(Json::ensureString(pkg, "name", QString())); + list->setExpectedChecksum(Json::ensureString(pkg, "sha256", QString())); + + return list; + } + + MetaVersion::Ptr buildVersionFromJson(const QString& componentUid, const QJsonObject& obj, bool markStability) + { + QString verId = Json::requireString(obj, "version"); + auto ver = std::make_shared(componentUid, verId); + + QString timeStr = Json::requireString(obj, "releaseTime"); + QDateTime dt = QDateTime::fromString(timeStr, Qt::ISODate); + ver->setReleaseTimestamp(dt.toMSecsSinceEpoch() / 1000); + + ver->setReleaseType(Json::ensureString(obj, "type", QString())); + ver->setStable(Json::ensureBoolean(obj, QStringLiteral("recommended"), false)); + ver->setVolatile(Json::ensureBoolean(obj, QStringLiteral("volatile"), false)); + + DependencySet deps = parseDependencies(obj, "requires"); + DependencySet conflicts = parseDependencies(obj, "conflicts"); + ver->setDependencies(deps, conflicts); + + QString sha = Json::ensureString(obj, "sha256", QString()); + if (!sha.isEmpty()) + ver->setExpectedChecksum(sha); + + if (markStability) + ver->markAsStableCandidate(); + + return ver; + } + + MetaVersion::Ptr buildFullVersionFromJson(const QJsonObject& obj) + { + QString uid = Json::requireString(obj, "uid"); + auto ver = buildVersionFromJson(uid, obj, false); + + VersionFilePtr data = + OneSixVersionFormat::versionFileFromJson(QJsonDocument(obj), + QString("%1/%2.json").arg(uid, ver->versionId()), + obj.contains("order")); + ver->setDetailedData(data); + + return ver; + } + + MetaVersionList::Ptr buildVersionListFromPackageJson(const QJsonObject& obj) + { + QString uid = Json::requireString(obj, "uid"); + + QList versionObjects = Json::requireIsArrayOf(obj, "versions"); + QList versions; + versions.reserve(versionObjects.size()); + + for (const QJsonObject& vObj : versionObjects) + versions.append(buildVersionFromJson(uid, vObj, true)); + + auto list = std::make_shared(uid); + list->setDisplayName(Json::ensureString(obj, "name", QString())); + list->setVersionEntries(versions); + + return list; + } + + std::shared_ptr buildIndexFromJson(const QJsonObject& obj) + { + QList packages = Json::requireIsArrayOf(obj, "packages"); + QList components; + components.reserve(packages.size()); + + for (const QJsonObject& pkg : packages) + components.append(buildVersionListFromPackage(pkg)); + + return std::make_shared(components); + } + + } // anonymous namespace + + void loadIndexFromJson(const QJsonObject& json, MetaIndex* index) + { + SchemaVersion schema = detectSchemaVersion(json); + + if (schema == SchemaVersion::V1) + index->mergeWith(buildIndexFromJson(json)); + else + throw MetaParseError(QObject::tr("Unsupported metadata schema version")); + } + + void loadVersionListFromJson(const QJsonObject& json, MetaVersionList* list) + { + SchemaVersion schema = detectSchemaVersion(json); + + if (schema == SchemaVersion::V1) + list->mergeWith(buildVersionListFromPackageJson(json)); + else + throw MetaParseError(QObject::tr("Unsupported metadata schema version")); + } + + void loadVersionFromJson(const QJsonObject& json, MetaVersion* version) + { + SchemaVersion schema = detectSchemaVersion(json); + + if (schema == SchemaVersion::V1) + version->updateFrom(buildFullVersionFromJson(json)); + else + throw MetaParseError(QObject::tr("Unsupported metadata schema version")); + } + +} // namespace projt::meta diff --git a/archived/projt-launcher/launcher/meta/JsonFormat.hpp b/archived/projt-launcher/launcher/meta/JsonFormat.hpp new file mode 100644 index 0000000000..18a6dd3775 --- /dev/null +++ b/archived/projt-launcher/launcher/meta/JsonFormat.hpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "Exception.h" + +namespace projt::meta +{ + + class MetaIndex; + class MetaVersion; + class MetaVersionList; + + enum class SchemaVersion + { + Unknown = -1, + V1 = 1 + }; + + class MetaParseError : public Exception + { + public: + using Exception::Exception; + }; + + struct ComponentDependency + { + QString uid; + QString equalsVersion; + QString suggests; + + bool operator==(const ComponentDependency& other) const + { + return uid == other.uid; + } + bool operator<(const ComponentDependency& other) const + { + return uid < other.uid; + } + + bool deepEquals(const ComponentDependency& other) const + { + return uid == other.uid && equalsVersion == other.equalsVersion && suggests == other.suggests; + } + }; + + using DependencySet = std::set; + + SchemaVersion detectSchemaVersion(const QJsonObject& root, bool required = true); + void writeSchemaVersion(QJsonObject& target, SchemaVersion version); + SchemaVersion currentSchema(); + + DependencySet parseDependencies(const QJsonObject& source, const QString& fieldName = "requires"); + void writeDependencies(QJsonObject& target, const DependencySet& deps, const QString& fieldName = "requires"); + + void loadIndexFromJson(const QJsonObject& json, MetaIndex* index); + void loadVersionFromJson(const QJsonObject& json, MetaVersion* version); + void loadVersionListFromJson(const QJsonObject& json, MetaVersionList* list); + +} // namespace projt::meta + +Q_DECLARE_METATYPE(std::set) diff --git a/archived/projt-launcher/launcher/meta/Version.cpp b/archived/projt-launcher/launcher/meta/Version.cpp new file mode 100644 index 0000000000..69fe3ab5f1 --- /dev/null +++ b/archived/projt-launcher/launcher/meta/Version.cpp @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "Version.hpp" + +#include +#include + +#include "JsonFormat.hpp" + +namespace projt::meta +{ + + MetaVersion::MetaVersion(const QString& componentUid, const QString& versionId) + : m_componentUid(componentUid), + m_versionId(versionId) + {} + + QString MetaVersion::name() const + { + if (m_detailedData) + return m_detailedData->name; + return m_componentUid; + } + + QDateTime MetaVersion::releaseTime() const + { + return QDateTime::fromMSecsSinceEpoch(m_timestamp * 1000, Qt::UTC); + } + + QString MetaVersion::parentComponentVersion() const + { + auto mcDep = std::find_if(m_dependencies.begin(), + m_dependencies.end(), + [](const ComponentDependency& d) + { return d.uid == "net.minecraft" && !d.equalsVersion.isEmpty(); }); + + if (mcDep != m_dependencies.end()) + return mcDep->equalsVersion; + + auto anyDep = std::find_if(m_dependencies.begin(), + m_dependencies.end(), + [](const ComponentDependency& d) { return !d.equalsVersion.isEmpty(); }); + + if (anyDep != m_dependencies.end()) + return anyDep->equalsVersion; + + return QString(); + } + + QString MetaVersion::cacheFilePath() const + { + return m_componentUid + "/" + m_versionId + ".json"; + } + + void MetaVersion::loadFromJson(const QJsonObject& json) + { + loadVersionFromJson(json, this); + } + + void MetaVersion::updateMetadataFrom(const MetaVersion::Ptr& source) + { + if (source->m_providesStability && m_stable != source->m_stable) + setStable(source->m_stable); + + if (m_releaseType != source->m_releaseType) + setReleaseType(source->m_releaseType); + + if (m_timestamp != source->m_timestamp) + setReleaseTimestamp(source->m_timestamp); + + if (m_dependencies != source->m_dependencies || m_conflicts != source->m_conflicts) + { + m_dependencies = source->m_dependencies; + m_conflicts = source->m_conflicts; + emit dependenciesChanged(); + } + + if (m_volatile != source->m_volatile) + setVolatile(source->m_volatile); + + if (!source->m_expectedSha256.isEmpty()) + m_expectedSha256 = source->m_expectedSha256; + } + + void MetaVersion::updateFrom(const MetaVersion::Ptr& source) + { + updateMetadataFrom(source); + + if (source->m_detailedData) + setDetailedData(source->m_detailedData); + } + + void MetaVersion::setReleaseType(const QString& type) + { + m_releaseType = type; + emit releaseTypeChanged(); + } + + void MetaVersion::setReleaseTimestamp(qint64 ts) + { + m_timestamp = ts; + emit timestampChanged(); + } + + void MetaVersion::setDependencies(const DependencySet& deps, const DependencySet& conflicts) + { + m_dependencies = deps; + m_conflicts = conflicts; + emit dependenciesChanged(); + } + + void MetaVersion::setStable(bool stable) + { + m_stable = stable; + } + + void MetaVersion::setVolatile(bool vol) + { + m_volatile = vol; + } + + void MetaVersion::setDetailedData(const VersionFilePtr& data) + { + m_detailedData = data; + } + + void MetaVersion::markAsStableCandidate() + { + m_providesStability = true; + } + +} // namespace projt::meta diff --git a/archived/projt-launcher/launcher/meta/Version.hpp b/archived/projt-launcher/launcher/meta/Version.hpp new file mode 100644 index 0000000000..6709df35e9 --- /dev/null +++ b/archived/projt-launcher/launcher/meta/Version.hpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "BaseEntity.hpp" +#include "BaseVersion.h" +#include "JsonFormat.hpp" +#include "Version.h" +#include "minecraft/VersionFile.h" + +namespace projt::meta +{ + + /** + * @brief Single version entry in the metadata system. + */ + class MetaVersion : public QObject, public BaseVersion, public MetaEntity + { + Q_OBJECT + + public: + using Ptr = std::shared_ptr; + + MetaVersion(const QString& componentUid, const QString& versionId); + ~MetaVersion() override = default; + + // BaseVersion interface + QString descriptor() const override + { + return m_versionId; + } + QString name() const override; + QString typeString() const override + { + return m_releaseType; + } + + // Identity + QString componentUid() const + { + return m_componentUid; + } + QString versionId() const + { + return m_versionId; + } + + // Release info + QString releaseType() const + { + return m_releaseType; + } + QDateTime releaseTime() const; + qint64 releaseTimestamp() const + { + return m_timestamp; + } + + // Flags + bool isStable() const + { + return m_stable; + } + bool isVolatile() const + { + return m_volatile; + } + + // Dependencies + const DependencySet& dependencies() const + { + return m_dependencies; + } + const DependencySet& conflicts() const + { + return m_conflicts; + } + QString parentComponentVersion() const; + + // Detailed data + bool hasDetailedData() const + { + return m_detailedData != nullptr; + } + const VersionFilePtr& detailedData() const + { + return m_detailedData; + } + bool isFullyLoaded() const + { + return hasDetailedData() && isReady(); + } + + // MetaEntity interface + QString cacheFilePath() const override; + void loadFromJson(const QJsonObject& json) override; + + // Version comparison + ::Version asComparableVersion() const + { + return ::Version(m_versionId); + } + + // Merge operations + void updateFrom(const MetaVersion::Ptr& source); + void updateMetadataFrom(const MetaVersion::Ptr& source); + + public: // Setters for parsers + void setReleaseType(const QString& type); + void setReleaseTimestamp(qint64 ts); + void setDependencies(const DependencySet& deps, const DependencySet& conflicts); + void setStable(bool stable); + void setVolatile(bool vol); + void setDetailedData(const VersionFilePtr& data); + void markAsStableCandidate(); + + signals: + void releaseTypeChanged(); + void timestampChanged(); + void dependenciesChanged(); + + private: + QString m_componentUid; + QString m_versionId; + QString m_releaseType; + qint64 m_timestamp = 0; + + bool m_stable = false; + bool m_volatile = false; + bool m_providesStability = false; + + DependencySet m_dependencies; + DependencySet m_conflicts; + + VersionFilePtr m_detailedData; + }; + +} // namespace projt::meta + +Q_DECLARE_METATYPE(projt::meta::MetaVersion::Ptr) diff --git a/archived/projt-launcher/launcher/meta/VersionList.cpp b/archived/projt-launcher/launcher/meta/VersionList.cpp new file mode 100644 index 0000000000..f1054d0d1a --- /dev/null +++ b/archived/projt-launcher/launcher/meta/VersionList.cpp @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "VersionList.hpp" + +#include +#include +#include + +#include "Application.h" +#include "Index.hpp" +#include "JsonFormat.hpp" +#include "net/Mode.h" +#include "tasks/SequentialTask.h" + +namespace projt::meta +{ + + MetaVersionList::MetaVersionList(const QString& componentUid, QObject* parent) + : BaseVersionList(parent), + m_uid(componentUid) + { + setObjectName("MetaVersionList: " + componentUid); + + m_activeRoles = { VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, + TypeRole, ComponentUidRole, TimestampRole, DependencyRole, + SortRole, RecommendedRole, LatestRole, VersionPtrRole }; + } + + bool MetaVersionList::isLoaded() + { + return MetaEntity::isReady(); + } + + Task::Ptr MetaVersionList::getLoadTask() + { + auto seq = makeShared(tr("Load metadata for %1").arg(m_uid)); + seq->addTask(APPLICATION->metadataIndex()->createLoadTask(Net::Mode::Online)); + seq->addTask(this->createLoadTask(Net::Mode::Online)); + return seq; + } + + const BaseVersion::Ptr MetaVersionList::at(int i) const + { + return m_entries.at(i); + } + + int MetaVersionList::count() const + { + return m_entries.size(); + } + + void MetaVersionList::sortVersions() + { + beginResetModel(); + std::sort(m_entries.begin(), + m_entries.end(), + [](const MetaVersion::Ptr& a, const MetaVersion::Ptr& b) + { return a->asComparableVersion() < b->asComparableVersion(); }); + endResetModel(); + } + + QVariant MetaVersionList::data(const QModelIndex& idx, int role) const + { + if (!idx.isValid() || idx.row() < 0 || idx.row() >= m_entries.size()) + return QVariant(); + + const MetaVersion::Ptr& ver = m_entries.at(idx.row()); + + switch (role) + { + case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast(ver)); + case VersionRole: + case VersionIdRole: return ver->versionId(); + case ParentVersionRole: + { + QString parent = ver->parentComponentVersion(); + return parent.isEmpty() ? QVariant() : parent; + } + case TypeRole: return ver->releaseType(); + case ComponentUidRole: return ver->componentUid(); + case TimestampRole: return ver->releaseTime(); + case DependencyRole: return QVariant::fromValue(ver->dependencies()); + case SortRole: return ver->releaseTimestamp(); + case VersionPtrRole: return QVariant::fromValue(ver); + case RecommendedRole: return ver->isStable() || m_externalStableVersions.contains(ver->versionId()); + case JavaMajorRole: + { + QString display = ver->versionId(); + if (display.startsWith("java")) + display = "Java " + display.mid(4); + return display; + } + default: return QVariant(); + } + } + + BaseVersionList::RoleList MetaVersionList::providesRoles() const + { + return m_activeRoles; + } + + void MetaVersionList::setAvailableRoles(RoleList roles) + { + m_activeRoles = roles; + } + + QHash MetaVersionList::roleNames() const + { + QHash names = BaseVersionList::roleNames(); + names.insert(ComponentUidRole, "componentUid"); + names.insert(TimestampRole, "timestamp"); + names.insert(SortRole, "sortKey"); + names.insert(DependencyRole, "dependencies"); + return names; + } + + QString MetaVersionList::cacheFilePath() const + { + return m_uid + "/index.json"; + } + + void MetaVersionList::loadFromJson(const QJsonObject& json) + { + loadVersionListFromJson(json, this); + } + + MetaVersion::Ptr MetaVersionList::getMetaVersion(const QString& versionId) + { + return m_versionIndex.value(versionId, nullptr); + } + + MetaVersion::Ptr MetaVersionList::getOrCreateVersion(const QString& versionId) + { + MetaVersion::Ptr existing = m_versionIndex.value(versionId); + if (existing) + return existing; + + auto newVer = std::make_shared(m_uid, versionId); + int row = m_entries.size(); + beginInsertRows(QModelIndex(), row, row); + m_versionIndex.insert(versionId, newVer); + registerVersion(row, newVer); + m_entries.append(newVer); + endInsertRows(); + + return newVer; + } + + bool MetaVersionList::containsVersion(const QString& versionId) const + { + return m_versionIndex.contains(versionId); + } + + void MetaVersionList::setDisplayName(const QString& name) + { + m_displayName = name; + emit displayNameChanged(name); + } + + void MetaVersionList::setVersionEntries(const QList& entries) + { + beginResetModel(); + + m_entries = entries; + + std::sort(m_entries.begin(), + m_entries.end(), + [](const MetaVersion::Ptr& a, const MetaVersion::Ptr& b) + { return a->releaseTimestamp() > b->releaseTimestamp(); }); + + m_versionIndex.clear(); + for (int i = 0; i < m_entries.size(); ++i) + { + const auto& ver = m_entries.at(i); + m_versionIndex.insert(ver->versionId(), ver); + registerVersion(i, ver); + } + + auto stableIt = std::find_if(m_entries.constBegin(), + m_entries.constEnd(), + [](const MetaVersion::Ptr& v) { return v->releaseType() == "release"; }); + + m_stableVersion = (stableIt != m_entries.constEnd()) ? *stableIt : nullptr; + + endResetModel(); + } + + void MetaVersionList::addExternalStableVersions(const QStringList& versions) + { + m_externalStableVersions.append(versions); + } + + void MetaVersionList::clearExternalStableVersions() + { + m_externalStableVersions.clear(); + } + + const MetaVersion::Ptr& MetaVersionList::selectBetterVersion(const MetaVersion::Ptr& a, const MetaVersion::Ptr& b) + { + if (!a) + return b; + if (!b) + return a; + + if (a->releaseType() != b->releaseType()) + { + if (a->releaseType() == "release") + return a; + if (b->releaseType() == "release") + return b; + } + + return (a->releaseTimestamp() > b->releaseTimestamp()) ? a : b; + } + + void MetaVersionList::updateFromIndex(const MetaVersionList::Ptr& other) + { + if (m_displayName != other->m_displayName) + setDisplayName(other->m_displayName); + + if (!other->m_expectedSha256.isEmpty()) + m_expectedSha256 = other->m_expectedSha256; + } + + void MetaVersionList::mergeWith(const MetaVersionList::Ptr& other) + { + if (m_displayName != other->m_displayName) + setDisplayName(other->m_displayName); + + if (!other->m_expectedSha256.isEmpty()) + m_expectedSha256 = other->m_expectedSha256; + + beginResetModel(); + + for (const auto& incoming : other->m_entries) + { + MetaVersion::Ptr existing = m_versionIndex.value(incoming->versionId()); + + if (existing) + { + existing->updateMetadataFrom(incoming); + } + else + { + m_versionIndex.insert(incoming->versionId(), incoming); + int row = m_entries.size(); + registerVersion(row, incoming); + m_entries.append(incoming); + } + + m_stableVersion = selectBetterVersion(m_stableVersion, incoming); + } + + endResetModel(); + } + + void MetaVersionList::registerVersion(int row, const MetaVersion::Ptr& version) + { + disconnect(version.get(), &MetaVersion::dependenciesChanged, this, nullptr); + disconnect(version.get(), &MetaVersion::timestampChanged, this, nullptr); + disconnect(version.get(), &MetaVersion::releaseTypeChanged, this, nullptr); + + connect(version.get(), + &MetaVersion::dependenciesChanged, + this, + [this, row]() { emit dataChanged(index(row), index(row), { DependencyRole }); }); + connect(version.get(), + &MetaVersion::timestampChanged, + this, + [this, row]() { emit dataChanged(index(row), index(row), { TimestampRole, SortRole }); }); + connect(version.get(), + &MetaVersion::releaseTypeChanged, + this, + [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); + } + + BaseVersion::Ptr MetaVersionList::getRecommended() const + { + return m_stableVersion; + } + + void MetaVersionList::waitUntilReady() + { + if (isLoaded()) + return; + + QEventLoop loop; + auto task = getLoadTask(); + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); + task->start(); + loop.exec(); + } + + MetaVersion::Ptr MetaVersionList::stableForParent(const QString& parentUid, const QString& parentVersion) + { + for (const auto& ver : m_entries) + { + if (!ver->isStable()) + continue; + + const auto& deps = ver->dependencies(); + auto match = std::find_if(deps.begin(), + deps.end(), + [&](const ComponentDependency& d) + { return d.uid == parentUid && d.equalsVersion == parentVersion; }); + + if (match != deps.end()) + return ver; + } + return nullptr; + } + + MetaVersion::Ptr MetaVersionList::latestForParent(const QString& parentUid, const QString& parentVersion) + { + MetaVersion::Ptr best = nullptr; + + for (const auto& ver : m_entries) + { + const auto& deps = ver->dependencies(); + auto match = std::find_if(deps.begin(), + deps.end(), + [&](const ComponentDependency& d) + { return d.uid == parentUid && d.equalsVersion == parentVersion; }); + + if (match != deps.end()) + best = selectBetterVersion(best, ver); + } + + return best; + } + +} // namespace projt::meta diff --git a/archived/projt-launcher/launcher/meta/VersionList.hpp b/archived/projt-launcher/launcher/meta/VersionList.hpp new file mode 100644 index 0000000000..febee1b4d9 --- /dev/null +++ b/archived/projt-launcher/launcher/meta/VersionList.hpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "BaseEntity.hpp" +#include "BaseVersionList.h" +#include "Version.hpp" + +namespace projt::meta +{ + + /** + * @brief Collection of MetaVersion entries for a single component. + */ + class MetaVersionList : public BaseVersionList, public MetaEntity + { + Q_OBJECT + Q_PROPERTY(QString uid READ uid CONSTANT) + Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) + + public: + using Ptr = std::shared_ptr; + + explicit MetaVersionList(const QString& componentUid, QObject* parent = nullptr); + ~MetaVersionList() override = default; + + enum Roles + { + ComponentUidRole = Qt::UserRole + 200, + TimestampRole, + DependencyRole, + VersionPtrRole + }; + + // BaseVersionList interface + bool isLoaded() override; + Task::Ptr getLoadTask() override; + const BaseVersion::Ptr at(int i) const override; + int count() const override; + void sortVersions() override; + BaseVersion::Ptr getRecommended() const override; + QVariant data(const QModelIndex& index, int role) const override; + RoleList providesRoles() const override; + QHash roleNames() const override; + + // MetaEntity interface + QString cacheFilePath() const override; + void loadFromJson(const QJsonObject& json) override; + + // Identity + QString uid() const + { + return m_uid; + } + QString displayName() const + { + return m_displayName.isEmpty() ? m_uid : m_displayName; + } + + // Version access + MetaVersion::Ptr getMetaVersion(const QString& versionId); + MetaVersion::Ptr getOrCreateVersion(const QString& versionId); + bool containsVersion(const QString& versionId) const; + QList allVersions() const + { + return m_entries; + } + + // Filtered lookups + MetaVersion::Ptr stableForParent(const QString& parentUid, const QString& parentVersion); + MetaVersion::Ptr latestForParent(const QString& parentUid, const QString& parentVersion); + + // External stable versions + void addExternalStableVersions(const QStringList& versions); + void clearExternalStableVersions(); + + // Blocking load + void waitUntilReady(); + + // Configuration + void setAvailableRoles(RoleList roles); + + public: // For parsers + void setDisplayName(const QString& name); + void setVersionEntries(const QList& entries); + void mergeWith(const MetaVersionList::Ptr& other); + void updateFromIndex(const MetaVersionList::Ptr& other); + + signals: + void displayNameChanged(const QString& name); + + protected slots: + void updateListData(QList) override + {} + + private: + void registerVersion(int row, const MetaVersion::Ptr& version); + static const MetaVersion::Ptr& selectBetterVersion(const MetaVersion::Ptr& a, const MetaVersion::Ptr& b); + + QString m_uid; + QString m_displayName; + + QList m_entries; + QHash m_versionIndex; + QStringList m_externalStableVersions; + + MetaVersion::Ptr m_stableVersion; + RoleList m_activeRoles; + }; + +} // namespace projt::meta + +Q_DECLARE_METATYPE(projt::meta::MetaVersionList::Ptr) diff --git a/archived/projt-launcher/launcher/minecraft/Agent.h b/archived/projt-launcher/launcher/minecraft/Agent.h new file mode 100644 index 0000000000..72e48d81a9 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Agent.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +#include "Library.h" + +class Agent; + +using AgentPtr = std::shared_ptr; + +class Agent +{ + public: + Agent(LibraryPtr library, const QString& argument) + { + m_library = library; + m_argument = argument; + } + + public: /* methods */ + LibraryPtr library() + { + return m_library; + } + QString argument() + { + return m_argument; + } + + protected: /* data */ + /// The library pointing to the jar this Java agent is contained within + LibraryPtr m_library; + + /// The argument to the Java agent, passed after an = if present + QString m_argument; +}; diff --git a/archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp b/archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp new file mode 100644 index 0000000000..26f54ea321 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp @@ -0,0 +1,643 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AssetsUtils.h" +#include "BuildConfig.h" +#include "FileSystem.h" +#include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" +#include "net/Download.h" + +#include "Application.h" +#include "net/NetRequest.h" + +namespace +{ + // Helper function to get standard asset directory structure + struct AssetDirs + { + QDir assets; + QDir indexes; + QDir objects; + QDir virtualDir; + }; + + AssetDirs getAssetDirectories() + { + AssetDirs dirs; + dirs.assets = QDir("assets/"); + dirs.indexes = QDir(FS::PathCombine(dirs.assets.path(), "indexes")); + dirs.objects = QDir(FS::PathCombine(dirs.assets.path(), "objects")); + dirs.virtualDir = QDir(FS::PathCombine(dirs.assets.path(), "virtual")); + return dirs; + } + + QSet collectPathsFromDir(QString dirPath) + { + QFileInfo dirInfo(dirPath); + + if (!dirInfo.exists()) + { + return {}; + } + + QSet out; + + QDirIterator iter(dirPath, QDirIterator::Subdirectories); + while (iter.hasNext()) + { + QString value = iter.next(); + QFileInfo info(value); + if (info.isFile()) + { + out.insert(value); + qDebug() << value; + } + } + return out; + } +} // namespace + +namespace AssetsUtils +{ + + /* + * Returns true on success, with index populated + * index is undefined otherwise + */ + bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsIndex& index) + { + /* + { + "objects": { + "icons/icon_16x16.png": { + "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", + "size": 3665 + }, + ... + } + } + } + */ + + QFile file(path); + + // Try to open the file and fail if we can't. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to read assets index file" << path << ":" << file.errorString(); + return false; + } + index.id = assetsId; + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << "Failed to parse assets index file:" << parseError.errorString() << "at offset " + << QString::number(parseError.offset); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid assets index JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + QJsonValue isVirtual = root.value("virtual"); + if (!isVirtual.isUndefined()) + { + index.isVirtual = isVirtual.toBool(false); + } + + QJsonValue mapToResources = root.value("map_to_resources"); + if (!mapToResources.isUndefined()) + { + index.mapToResources = mapToResources.toBool(false); + } + + QJsonValue objects = root.value("objects"); + QVariantMap map = objects.toVariant().toMap(); + + for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) + { + // qDebug() << iter.key(); + + QVariant variant = iter.value(); + QVariantMap nested_objects = variant.toMap(); + + AssetObject object; + + for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); nested_iter != nested_objects.end(); + ++nested_iter) + { + // qDebug() << nested_iter.key() << nested_iter.value().toString(); + QString key = nested_iter.key(); + QVariant value = nested_iter.value(); + + if (key == "hash") + { + object.hash = value.toString(); + } + else if (key == "size") + { + object.size = value.toLongLong(); + } + } + + index.objects.insert(iter.key(), object); + } + + return true; + } + + QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder) + { + auto dirs = getAssetDirectories(); + QDir indexDir = dirs.indexes; + QDir objectDir = dirs.objects; + QDir virtualDir = dirs.virtualDir; + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't determine assets path!"; + return virtualRoot; + } + + AssetsIndex index; + if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!"; + return virtualRoot; + } + + QString targetPath; + if (index.isVirtual) + { + return virtualRoot; + } + else if (index.mapToResources) + { + return QDir(resourcesFolder); + } + return virtualRoot; + } + + bool reconstructAssets(QString assetsId, QString resourcesFolder) + { + auto dirs = getAssetDirectories(); + QDir indexDir = dirs.indexes; + QDir objectDir = dirs.objects; + QDir virtualDir = QDir(FS::PathCombine(dirs.assets.path(), "virtual")); + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!"; + return false; + } + + qDebug() << "reconstructAssets" << dirs.assets.path() << indexDir.path() << objectDir.path() + << virtualDir.path() << virtualRoot.path(); + + AssetsIndex index; + if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!"; + return false; + } + + QString targetPath; + bool removeLeftovers = false; + if (index.isVirtual) + { + targetPath = virtualRoot.path(); + removeLeftovers = true; + qDebug() << "Reconstructing virtual assets folder at" << targetPath; + } + else if (index.mapToResources) + { + targetPath = resourcesFolder; + qDebug() << "Reconstructing resources folder at" << targetPath; + } + + if (!targetPath.isNull()) + { + auto presentFiles = collectPathsFromDir(targetPath); + for (QString map : index.objects.keys()) + { + AssetObject asset_object = index.objects.value(map); + QString target_path = FS::PathCombine(targetPath, map); + QFile target(target_path); + + QString tlk = asset_object.hash.left(2); + + QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash); + QFile original(original_path); + if (!original.exists()) + continue; + + presentFiles.remove(target_path); + + if (!target.exists()) + { + QFileInfo info(target_path); + QDir target_dir = info.dir(); + + qDebug() << target_dir.path(); + FS::ensureFolderPathExists(target_dir.path()); + + bool couldCopy = original.copy(target_path); + qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy); + } + } + + // Consider adding a function to update the .lastused file with the current timestamp for asset usage + // tracking. + if (removeLeftovers) + { + for (auto& file : presentFiles) + { + qDebug() << "Would remove" << file; + } + } + } + return true; + } + +} // namespace AssetsUtils + +// ============================================================================ +// AssetObject implementation +// ============================================================================ + +QString AssetObject::getRelPath() const +{ + return hash.left(2) + "/" + hash; +} + +QString AssetObject::getLocalPath() const +{ + return "assets/objects/" + getRelPath(); +} + +QUrl AssetObject::getUrl() const +{ + auto resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); + if (resourceURL.isEmpty()) + { + resourceURL = BuildConfig.DEFAULT_RESOURCE_BASE; + } + return resourceURL + getRelPath(); +} + +bool AssetObject::isValid() const +{ + QFileInfo objectFile(getLocalPath()); + return objectFile.isFile() && objectFile.size() == size; +} + +Net::NetRequest::Ptr AssetObject::getDownloadAction() const +{ + if (isValid()) + { + return nullptr; + } + + QFileInfo objectFile(getLocalPath()); + auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath()); + if (!hash.isEmpty()) + { + objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, hash)); + } + objectDL->setProgress(objectDL->getProgress(), size); + return objectDL; +} + +// ============================================================================ +// AssetsIndex implementation +// ============================================================================ + +NetJob::Ptr AssetsIndex::getDownloadJob() const +{ + auto job = makeShared(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); + for (const auto& object : objects.values()) + { + auto dl = object.getDownloadAction(); + if (dl) + { + job->addNetAction(dl); + } + } + return job->size() ? job : nullptr; +} + +qint64 AssetsIndex::totalSize() const +{ + qint64 total = 0; + for (const auto& object : objects.values()) + { + total += object.size; + } + return total; +} + +int AssetsIndex::missingAssetCount() const +{ + int count = 0; + for (const auto& object : objects.values()) + { + if (!object.isValid()) + { + count++; + } + } + return count; +} + +bool AssetsIndex::isComplete() const +{ + return missingAssetCount() == 0; +} + +// ============================================================================ +// AssetsManager implementation +// ============================================================================ + +QDir AssetsManager::assetsBaseDir() +{ + return QDir("assets/"); +} + +QDir AssetsManager::indexesDir() +{ + return QDir(FS::PathCombine(assetsBaseDir().path(), "indexes")); +} + +QDir AssetsManager::objectsDir() +{ + return QDir(FS::PathCombine(assetsBaseDir().path(), "objects")); +} + +QDir AssetsManager::virtualDir() +{ + return QDir(FS::PathCombine(assetsBaseDir().path(), "virtual")); +} + +std::optional AssetsManager::loadIndex(const QString& indexId, const QString& filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to read assets index file" << filePath << ":" << file.errorString(); + return std::nullopt; + } + + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << "Failed to parse assets index file:" << parseError.errorString() << "at offset" + << QString::number(parseError.offset); + return std::nullopt; + } + + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid assets index JSON: Root should be an object."; + return std::nullopt; + } + + QJsonObject root = jsonDoc.object(); + AssetsIndex index; + index.id = indexId; + + index.isVirtual = root.value("virtual").toBool(false); + index.mapToResources = root.value("map_to_resources").toBool(false); + + QJsonValue objectsValue = root.value("objects"); + QVariantMap map = objectsValue.toVariant().toMap(); + + for (auto iter = map.constBegin(); iter != map.constEnd(); ++iter) + { + QVariantMap nested = iter.value().toMap(); + AssetObject obj; + obj.hash = nested.value("hash").toString(); + obj.size = nested.value("size").toLongLong(); + + index.objects.insert(iter.key(), obj); + } + + return index; +} + +QDir AssetsManager::getAssetsDir(const AssetsIndex& index, const QString& resourcesFolder) +{ + if (index.isVirtual) + { + return QDir(FS::PathCombine(virtualDir().path(), index.id)); + } + else if (index.mapToResources) + { + return QDir(resourcesFolder); + } + return QDir(FS::PathCombine(virtualDir().path(), index.id)); +} + +QDir AssetsManager::getAssetsDir(const QString& assetsId, const QString& resourcesFolder) +{ + QString indexPath = FS::PathCombine(indexesDir().path(), assetsId + ".json"); + + auto index = loadIndex(assetsId, indexPath); + if (!index) + { + qCritical() << "Failed to load asset index" << assetsId; + return QDir(FS::PathCombine(virtualDir().path(), assetsId)); + } + + return getAssetsDir(*index, resourcesFolder); +} + +bool AssetsManager::reconstructAssets(const AssetsIndex& index, const QString& resourcesFolder) +{ + QString targetPath; + bool removeLeftovers = false; + + if (index.isVirtual) + { + targetPath = FS::PathCombine(virtualDir().path(), index.id); + removeLeftovers = true; + qDebug() << "Reconstructing virtual assets folder at" << targetPath; + } + else if (index.mapToResources) + { + targetPath = resourcesFolder; + qDebug() << "Reconstructing resources folder at" << targetPath; + } + + if (targetPath.isEmpty()) + { + return true; // Nothing to reconstruct + } + + // Collect existing files + QSet presentFiles = collectPathsFromDir(targetPath); + + // Copy missing assets + for (auto iter = index.objects.constBegin(); iter != index.objects.constEnd(); ++iter) + { + const QString& assetPath = iter.key(); + const AssetObject& asset = iter.value(); + + QString targetFilePath = FS::PathCombine(targetPath, assetPath); + QString tlk = asset.hash.left(2); + QString originalPath = FS::PathCombine(objectsDir().path(), tlk, asset.hash); + + QFile original(originalPath); + if (!original.exists()) + { + continue; + } + + presentFiles.remove(targetFilePath); + + QFile target(targetFilePath); + if (!target.exists()) + { + QFileInfo info(targetFilePath); + FS::ensureFolderPathExists(info.dir().path()); + + bool success = original.copy(targetFilePath); + qDebug() << "Copying" << originalPath << "to" << targetFilePath << (success ? "OK" : "FAILED"); + } + } + + // Remove leftover files (only for virtual assets) + if (removeLeftovers) + { + for (const auto& file : presentFiles) + { + qDebug() << "Removing leftover file:" << file; + QFile::remove(file); + } + } + + return true; +} + +bool AssetsManager::reconstructAssets(const QString& assetsId, const QString& resourcesFolder) +{ + QString indexPath = FS::PathCombine(indexesDir().path(), assetsId + ".json"); + + auto index = loadIndex(assetsId, indexPath); + if (!index) + { + qCritical() << "Failed to load asset index for reconstruction:" << assetsId; + return false; + } + + return reconstructAssets(*index, resourcesFolder); +} + +QStringList AssetsManager::validateAssets(const AssetsIndex& index) +{ + QStringList missing; + + for (auto iter = index.objects.constBegin(); iter != index.objects.constEnd(); ++iter) + { + const AssetObject& asset = iter.value(); + if (!asset.isValid()) + { + missing.append(iter.key()); + } + } + + return missing; +} diff --git a/archived/projt-launcher/launcher/minecraft/AssetsUtils.h b/archived/projt-launcher/launcher/minecraft/AssetsUtils.h new file mode 100644 index 0000000000..f5d2a4c440 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/AssetsUtils.h @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include "net/NetJob.h" +#include "net/NetRequest.h" + +/** Represents a single Minecraft asset object. + * Assets are stored by their SHA1 hash in a content-addressable storage. + */ +struct AssetObject +{ + QString hash; + qint64 size = 0; + + /** Get the relative path within the objects directory (e.g., "ab/abcdef..."). */ + QString getRelPath() const; + + /** Get the full download URL for this asset. */ + QUrl getUrl() const; + + /** Get the local filesystem path where this asset is stored. */ + QString getLocalPath() const; + + /** Create a download action for this asset if it needs to be downloaded. */ + Net::NetRequest::Ptr getDownloadAction() const; + + /** Check if the asset exists locally and has the correct size. */ + bool isValid() const; +}; + +/** Represents a Minecraft assets index. + * The index maps virtual paths to asset objects. + */ +struct AssetsIndex +{ + QString id; + QMap objects; + bool isVirtual = false; + bool mapToResources = false; + + /** Create a NetJob to download all missing assets. */ + NetJob::Ptr getDownloadJob() const; + + /** Get the total size of all assets in bytes. */ + qint64 totalSize() const; + + /** Get the number of assets that need to be downloaded. */ + int missingAssetCount() const; + + /** Check if all assets are present locally. */ + bool isComplete() const; +}; + +/** Asset management utilities. + * Provides functions for loading, validating, and reconstructing Minecraft assets. + */ +class AssetsManager +{ + public: + /** Load an assets index from a JSON file. + * @param id The asset index ID (e.g., "1.19", "legacy") + * @param filePath Path to the index JSON file + * @return The parsed AssetsIndex, or nullopt on failure + */ + static std::optional loadIndex(const QString& id, const QString& filePath); + + /** Get the appropriate assets directory for an index. + * Handles virtual assets, legacy resource mapping, and standard paths. + * @param index The assets index + * @param resourcesFolder Path to the resources folder (for legacy versions) + * @return The directory where assets should be accessed + */ + static QDir getAssetsDir(const AssetsIndex& index, const QString& resourcesFolder); + + /** Get the assets directory by ID (loads index internally). + * @param assetsId The asset index ID + * @param resourcesFolder Path to the resources folder + * @return The directory where assets should be accessed + */ + static QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder); + + /** Reconstruct virtual/legacy assets from the object store. + * Copies assets from hash-based storage to named paths for older MC versions. + * @param index The assets index to reconstruct + * @param resourcesFolder Path to the resources folder + * @return true if reconstruction succeeded + */ + static bool reconstructAssets(const AssetsIndex& index, const QString& resourcesFolder); + + /** Reconstruct assets by ID (loads index internally). */ + static bool reconstructAssets(const QString& assetsId, const QString& resourcesFolder); + + /** Get the standard paths for asset storage. */ + static QDir assetsBaseDir(); + static QDir indexesDir(); + static QDir objectsDir(); + static QDir virtualDir(); + + /** Validate the integrity of all assets in an index. + * Checks that all files exist and have correct sizes. + * @param index The assets index to validate + * @return List of missing or invalid asset paths + */ + static QStringList validateAssets(const AssetsIndex& index); +}; + +// Legacy namespace - delegates to AssetsManager +namespace AssetsUtils +{ + bool loadAssetsIndexJson(const QString& id, const QString& file, AssetsIndex& index); + QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder); + bool reconstructAssets(QString assetsId, QString resourcesFolder); +} diff --git a/archived/projt-launcher/launcher/minecraft/BackupManager.cpp b/archived/projt-launcher/launcher/minecraft/BackupManager.cpp new file mode 100644 index 0000000000..9842cb7720 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/BackupManager.cpp @@ -0,0 +1,594 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "BackupManager.h" +#include +#include +#include +#include +#include +#include +#include +#include "FileSystem.h" +#include "MMCZip.h" +#include "MinecraftInstance.h" + +// BackupOptions implementation +qint64 BackupOptions::estimateSize() const +{ + // Rough estimation based on typical Minecraft instance sizes + // Note: This is a rough estimate for UI display. Actual size may vary. + qint64 size = 0; + if (includeSaves) + size += 500LL * 1024 * 1024; // 500MB average for saves + if (includeConfig) + size += 50LL * 1024 * 1024; // 50MB for config + if (includeMods) + size += 1000LL * 1024 * 1024; // 1GB for mods + if (includeResourcePacks) + size += 200LL * 1024 * 1024; // 200MB + if (includeShaderPacks) + size += 100LL * 1024 * 1024; // 100MB + if (includeScreenshots) + size += 100LL * 1024 * 1024; // 100MB + if (includeOptions) + size += 1LL * 1024 * 1024; // 1MB + // Add custom paths roughly - assume 50MB per path as placeholder + size += customPaths.size() * 50LL * 1024 * 1024; // 50MB per custom path + return size; +} + +// InstanceBackup implementation +InstanceBackup::InstanceBackup(const QString& path) : m_backupPath(path) +{ + QFileInfo info(path); + if (!info.exists()) + { + return; + } + + m_size = info.size(); + m_createdAt = info.birthTime(); + m_name = info.completeBaseName(); + + // Try to load metadata + QString metaPath = path + ".json"; + if (QFile::exists(metaPath)) + { + QFile metaFile(metaPath); + if (metaFile.open(QIODevice::ReadOnly)) + { + QJsonDocument doc = QJsonDocument::fromJson(metaFile.readAll()); + QJsonObject obj = doc.object(); + + if (obj.contains("name")) + m_name = obj["name"].toString(); + if (obj.contains("description")) + m_description = obj["description"].toString(); + if (obj.contains("created")) + m_createdAt = QDateTime::fromString(obj["created"].toString(), Qt::ISODate); + if (obj.contains("includedPaths")) + m_includedPaths = obj["includedPaths"].toVariant().toStringList(); + } + } +} + +bool InstanceBackup::isValid() const +{ + return QFile::exists(m_backupPath) && m_size > 0; +} + +QString InstanceBackup::displaySize() const +{ + double size = m_size; + QStringList units = { "B", "KB", "MB", "GB" }; + int unitIndex = 0; + + while (size >= 1024.0 && unitIndex < units.size() - 1) + { + size /= 1024.0; + unitIndex++; + } + + return QString::number(size, 'f', 2) + " " + units[unitIndex]; +} + +// BackupManager implementation +BackupManager::BackupManager(QObject* parent) : QObject(parent) +{} + +QString BackupManager::getBackupDirectory(InstancePtr instance) +{ + if (!instance) + { + return QString(); + } + + QString backupDir = FS::PathCombine(instance->instanceRoot(), "backups"); + FS::ensureFolderPathExists(backupDir); + return backupDir; +} + +bool BackupManager::createBackup(InstancePtr instance, const QString& backupName, const BackupOptions& options) +{ + if (!instance) + { + qWarning() << "BackupManager: instance is null"; + return false; + } + + QString backupDir = getBackupDirectory(instance); + qDebug() << "BackupManager: backup directory:" << backupDir; + + QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss"); + QString safeName = backupName.isEmpty() ? timestamp : backupName + "_" + timestamp; + QString backupPath = FS::PathCombine(backupDir, safeName + ".zip"); + + qDebug() << "BackupManager: creating backup at:" << backupPath; + + // Create metadata + QJsonObject metadata; + metadata["name"] = backupName.isEmpty() ? timestamp : backupName; + metadata["created"] = QDateTime::currentDateTime().toString(Qt::ISODate); + metadata["instanceName"] = instance->name(); + + QStringList includedPaths; + if (options.includeSaves) + includedPaths << "saves"; + if (options.includeConfig) + includedPaths << "config"; + if (options.includeMods) + includedPaths << "mods"; + if (options.includeResourcePacks) + includedPaths << "resourcepacks"; + if (options.includeShaderPacks) + includedPaths << "shaderpacks"; + if (options.includeScreenshots) + includedPaths << "screenshots"; + if (options.includeOptions) + includedPaths << "options.txt" + << "optionsof.txt"; + + // Add custom paths to included paths list + includedPaths += options.customPaths; + + metadata["includedPaths"] = QJsonArray::fromStringList(includedPaths); + + // Save metadata + QFile metaFile(backupPath + ".json"); + if (metaFile.open(QIODevice::WriteOnly)) + { + metaFile.write(QJsonDocument(metadata).toJson()); + metaFile.close(); + } + else + { + qWarning() << "BackupManager: failed to write metadata file"; + } + + // Get the game root (where .minecraft files are) + auto minecraftInstance = std::dynamic_pointer_cast(instance); + QString gameRoot = minecraftInstance ? minecraftInstance->gameRoot() : instance->instanceRoot(); + + // Compress backup + qDebug() << "BackupManager: starting compression..."; + qDebug() << "BackupManager: game root is:" << gameRoot; + if (!compressBackup(gameRoot, backupPath, options)) + { + qWarning() << "BackupManager: compression failed"; + QFile::remove(backupPath); + QFile::remove(backupPath + ".json"); + return false; + } + + qDebug() << "BackupManager: backup created successfully"; + emit backupCreated(instance->id(), backupName); + return true; +} + +void BackupManager::createBackupAsync(InstancePtr instance, const QString& backupName, const BackupOptions& options) +{ + if (!instance) + { + emit backupFailed(QString(), "Instance is null"); + return; + } + + QString instanceId = instance->id(); + emit backupStarted(instanceId, backupName); + + // Run backup in background thread + auto future = QtConcurrent::run([this, instance, backupName, options]() + { return createBackup(instance, backupName, options); }); + + // Watch for completion + auto watcher = new QFutureWatcher(this); + connect(watcher, + &QFutureWatcher::finished, + this, + [this, watcher, instanceId, backupName]() + { + bool success = watcher->result(); + if (!success) + { + emit backupFailed(instanceId, "Backup creation failed"); + } + watcher->deleteLater(); + }); + watcher->setFuture(future); +} + +void BackupManager::restoreBackupAsync(InstancePtr instance, + const InstanceBackup& backup, + bool createBackupBeforeRestore) +{ + if (!instance) + { + emit restoreFailed(QString(), "Instance is null"); + return; + } + + QString instanceId = instance->id(); + QString backupName = backup.name(); + emit restoreStarted(instanceId, backupName); + + // Run restore in background thread + auto future = QtConcurrent::run([this, instance, backup, createBackupBeforeRestore]() + { return restoreBackup(instance, backup, createBackupBeforeRestore); }); + + // Watch for completion + auto watcher = new QFutureWatcher(this); + connect(watcher, + &QFutureWatcher::finished, + this, + [this, watcher, instanceId, backupName]() + { + bool success = watcher->result(); + if (!success) + { + emit restoreFailed(instanceId, "Backup restoration failed"); + } + watcher->deleteLater(); + }); + watcher->setFuture(future); +} + +bool BackupManager::compressBackup(const QString& sourcePath, const QString& backupPath, const BackupOptions& options) +{ + QFileInfoList files; + + qDebug() << "BackupManager: compressing from" << sourcePath << "to" << backupPath; + qDebug() << "BackupManager: backup options - saves:" << options.includeSaves << "config:" << options.includeConfig + << "mods:" << options.includeMods << "resourcepacks:" << options.includeResourcePacks + << "shaderpacks:" << options.includeShaderPacks << "screenshots:" << options.includeScreenshots + << "options:" << options.includeOptions << "customPaths:" << options.customPaths; + + // Helper lambda to recursively collect files + auto collectFilesRecursive = [&files](const QString& dirPath) + { + qDebug() << "BackupManager: scanning directory" << dirPath; + int count = 0; + QDirIterator it(dirPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (it.hasNext()) + { + QFileInfo info(it.next()); + files.append(info); + count++; + } + qDebug() << "BackupManager: found" << count << "files in" << dirPath; + }; + + if (options.includeSaves) + { + QString savesPath = FS::PathCombine(sourcePath, "saves"); + qDebug() << "BackupManager: checking saves path:" << savesPath; + if (QDir(savesPath).exists()) + { + collectFilesRecursive(savesPath); + } + else + { + qDebug() << "BackupManager: saves directory does not exist"; + } + } + if (options.includeConfig) + { + QString configPath = FS::PathCombine(sourcePath, "config"); + qDebug() << "BackupManager: checking config path:" << configPath; + if (QDir(configPath).exists()) + { + collectFilesRecursive(configPath); + } + else + { + qDebug() << "BackupManager: config directory does not exist"; + } + } + if (options.includeMods) + { + QString modsPath = FS::PathCombine(sourcePath, "mods"); + qDebug() << "BackupManager: checking mods path:" << modsPath; + if (QDir(modsPath).exists()) + { + collectFilesRecursive(modsPath); + } + else + { + qDebug() << "BackupManager: mods directory does not exist"; + } + } + if (options.includeResourcePacks) + { + QString rpPath = FS::PathCombine(sourcePath, "resourcepacks"); + qDebug() << "BackupManager: checking resourcepacks path:" << rpPath; + if (QDir(rpPath).exists()) + { + collectFilesRecursive(rpPath); + } + else + { + qDebug() << "BackupManager: resourcepacks directory does not exist"; + } + } + if (options.includeShaderPacks) + { + QString spPath = FS::PathCombine(sourcePath, "shaderpacks"); + qDebug() << "BackupManager: checking shaderpacks path:" << spPath; + if (QDir(spPath).exists()) + { + collectFilesRecursive(spPath); + } + else + { + qDebug() << "BackupManager: shaderpacks directory does not exist"; + } + } + if (options.includeScreenshots) + { + QString ssPath = FS::PathCombine(sourcePath, "screenshots"); + qDebug() << "BackupManager: checking screenshots path:" << ssPath; + if (QDir(ssPath).exists()) + { + collectFilesRecursive(ssPath); + } + else + { + qDebug() << "BackupManager: screenshots directory does not exist"; + } + } + if (options.includeOptions) + { + QFileInfo optionsFile(FS::PathCombine(sourcePath, "options.txt")); + qDebug() << "BackupManager: checking options.txt:" << optionsFile.absoluteFilePath(); + if (optionsFile.exists()) + { + qDebug() << "BackupManager: found options.txt"; + files.append(optionsFile); + } + else + { + qDebug() << "BackupManager: options.txt does not exist"; + } + QFileInfo optionsOfFile(FS::PathCombine(sourcePath, "optionsof.txt")); + qDebug() << "BackupManager: checking optionsof.txt:" << optionsOfFile.absoluteFilePath(); + if (optionsOfFile.exists()) + { + qDebug() << "BackupManager: found optionsof.txt"; + files.append(optionsOfFile); + } + else + { + qDebug() << "BackupManager: optionsof.txt does not exist"; + } + } + + // Include custom paths + for (const QString& customPath : options.customPaths) + { + QString fullPath = FS::PathCombine(sourcePath, customPath); + QFileInfo info(fullPath); + + if (info.isFile()) + { + files.append(info); + } + else if (info.isDir()) + { + collectFilesRecursive(fullPath); + } + } + + if (files.isEmpty()) + { + qWarning() << "BackupManager: no files to backup!"; + // For new instances with no files, consider backup successful to allow launch + return true; + } + + qDebug() << "BackupManager: collected" << files.size() << "files"; + + // Use MMCZip to compress - sourcePath is the base directory for relative paths + bool result = MMCZip::compressDirFiles(backupPath, sourcePath, files); + + if (!result) + { + qWarning() << "BackupManager: MMCZip::compressDirFiles failed"; + } + + return result; +} + +bool BackupManager::restoreBackup(InstancePtr instance, const InstanceBackup& backup, bool createBackupBeforeRestore) +{ + if (!instance || !backup.isValid()) + { + qWarning() << "BackupManager: invalid instance or backup"; + return false; + } + + // Get the game root (where .minecraft files are) + auto minecraftInstance = std::dynamic_pointer_cast(instance); + QString gameRoot = minecraftInstance ? minecraftInstance->gameRoot() : instance->instanceRoot(); + + qDebug() << "BackupManager: restoring backup to game root:" << gameRoot; + + // Create safety backup before restore + if (createBackupBeforeRestore) + { + qDebug() << "BackupManager: creating safety backup before restore"; + BackupOptions safetyOptions; + safetyOptions.includeSaves = true; + safetyOptions.includeConfig = true; + safetyOptions.includeMods = true; + safetyOptions.includeResourcePacks = true; + safetyOptions.includeShaderPacks = true; + safetyOptions.includeScreenshots = true; + safetyOptions.includeOptions = true; + createBackup(instance, "pre-restore_" + backup.name(), safetyOptions); + } + + // Extract backup + qDebug() << "BackupManager: extracting backup from" << backup.backupPath() << "to" << gameRoot; + if (!extractBackup(backup.backupPath(), gameRoot)) + { + qWarning() << "BackupManager: failed to extract backup"; + return false; + } + + qDebug() << "BackupManager: backup restored successfully"; + emit backupRestored(instance->id(), backup.name()); + return true; +} + +bool BackupManager::extractBackup(const QString& backupPath, const QString& targetPath) +{ + qDebug() << "BackupManager: extracting" << backupPath << "to" << targetPath; + + if (!QFile::exists(backupPath)) + { + qWarning() << "BackupManager: backup file does not exist:" << backupPath; + return false; + } + + auto result = MMCZip::extractDir(backupPath, targetPath); + + if (!result.has_value()) + { + qWarning() << "BackupManager: extraction failed"; + return false; + } + + qDebug() << "BackupManager: extraction successful"; + return true; +} + +QList BackupManager::listBackups(InstancePtr instance) const +{ + if (!instance) + { + return {}; + } + + QString backupDir = getBackupDirectory(instance); + return scanBackupDirectory(backupDir); +} + +QList BackupManager::scanBackupDirectory(const QString& backupDir) const +{ + QList backups; + QDir dir(backupDir); + + if (!dir.exists()) + { + return backups; + } + + QStringList zipFiles = dir.entryList(QStringList() << "*.zip", QDir::Files, QDir::Time); + + for (const QString& zipFile : zipFiles) + { + QString fullPath = dir.absoluteFilePath(zipFile); + InstanceBackup backup(fullPath); + + if (backup.isValid()) + { + backups.append(backup); + } + } + + return backups; +} + +int BackupManager::deleteOldBackups(InstancePtr instance, int maxCount) +{ + if (!instance || maxCount < 1) + { + return 0; + } + + QList backups = listBackups(instance); + + if (backups.size() <= maxCount) + { + return 0; + } + + // Sort by date (newest first) + std::sort(backups.begin(), + backups.end(), + [](const InstanceBackup& a, const InstanceBackup& b) { return a.createdAt() > b.createdAt(); }); + + int deletedCount = 0; + for (int i = maxCount; i < backups.size(); i++) + { + if (deleteBackup(backups[i])) + { + deletedCount++; + } + } + + return deletedCount; +} + +bool BackupManager::deleteBackup(const InstanceBackup& backup) +{ + if (!backup.isValid()) + { + return false; + } + + bool success = QFile::remove(backup.backupPath()); + QFile::remove(backup.backupPath() + ".json"); // Remove metadata too + + return success; +} + +bool BackupManager::autoBackupBeforeLaunch(InstancePtr instance) +{ + BackupOptions options; + options.includeSaves = true; + options.includeConfig = true; + options.includeOptions = true; + options.includeMods = false; // Don't backup mods by default (too large) + + return createBackup(instance, "auto-backup-pre-launch", options); +} diff --git a/archived/projt-launcher/launcher/minecraft/BackupManager.h b/archived/projt-launcher/launcher/minecraft/BackupManager.h new file mode 100644 index 0000000000..ec7b42df2f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/BackupManager.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "BaseInstance.h" + +struct BackupOptions +{ + bool includeSaves = true; + bool includeConfig = true; + bool includeMods = false; + bool includeResourcePacks = false; + bool includeShaderPacks = false; + bool includeScreenshots = false; + bool includeOptions = true; + QStringList customPaths; // Relative paths to include (e.g., "logs", "crash-reports") + + qint64 estimateSize() const; +}; + +class InstanceBackup +{ + public: + InstanceBackup() = default; + InstanceBackup(const QString& path); + + QString name() const + { + return m_name; + } + QString backupPath() const + { + return m_backupPath; + } + QDateTime createdAt() const + { + return m_createdAt; + } + qint64 size() const + { + return m_size; + } + QString description() const + { + return m_description; + } + QStringList includedPaths() const + { + return m_includedPaths; + } + + void setName(const QString& name) + { + m_name = name; + } + void setDescription(const QString& desc) + { + m_description = desc; + } + + bool isValid() const; + QString displaySize() const; + + private: + QString m_name; + QString m_backupPath; + QString m_description; + QDateTime m_createdAt; + qint64 m_size = 0; + QStringList m_includedPaths; + + friend class BackupManager; +}; + +class BackupManager : public QObject +{ + Q_OBJECT + + public: + explicit BackupManager(QObject* parent = nullptr); + + // Create a new backup (async) + void createBackupAsync(InstancePtr instance, const QString& backupName, const BackupOptions& options); + + // Create a new backup (blocking) + bool createBackup(InstancePtr instance, const QString& backupName, const BackupOptions& options); + + // Restore from backup (async) + void restoreBackupAsync(InstancePtr instance, const InstanceBackup& backup, bool createBackupBeforeRestore = true); + + // Restore from backup (blocking) + bool restoreBackup(InstancePtr instance, const InstanceBackup& backup, bool createBackupBeforeRestore = true); + + // List all backups for an instance + QList listBackups(InstancePtr instance) const; + + // Delete old backups (keep only maxCount newest) + int deleteOldBackups(InstancePtr instance, int maxCount); + + // Delete specific backup + bool deleteBackup(const InstanceBackup& backup); + + // Get backup directory for instance + static QString getBackupDirectory(InstancePtr instance); + + // Auto backup before launch + bool autoBackupBeforeLaunch(InstancePtr instance); + + signals: + void backupCreated(const QString& instanceId, const QString& backupName); + void backupRestored(const QString& instanceId, const QString& backupName); + void backupDeleted(const QString& instanceId, const QString& backupName); + void backupProgress(int current, int total, const QString& currentFile); + void backupStarted(const QString& instanceId, const QString& backupName); + void backupFailed(const QString& instanceId, const QString& error); + void restoreStarted(const QString& instanceId, const QString& backupName); + void restoreFailed(const QString& instanceId, const QString& error); + + private: + bool compressBackup(const QString& sourcePath, const QString& backupPath, const BackupOptions& options); + bool extractBackup(const QString& backupPath, const QString& targetPath); + QList scanBackupDirectory(const QString& backupDir) const; +}; \ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/Component.cpp b/archived/projt-launcher/launcher/minecraft/Component.cpp new file mode 100644 index 0000000000..2cb5054e3f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Component.cpp @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "Component.h" +#include +#include + +#include + +#include "Application.h" +#include "FileSystem.h" +#include "OneSixVersionFormat.h" +#include "VersionFile.h" +#include "meta/Version.hpp" +#include "minecraft/Component.h" +#include "minecraft/PackProfile.h" + +#include + +const QMap Component::KNOWN_MODLOADERS = { + { "net.neoforged", + { ModPlatform::NeoForge, { "net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.minecraftforge", + { ModPlatform::Forge, { "net.neoforged", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.fabricmc.fabric-loader", + { ModPlatform::Fabric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, + { "org.quiltmc.quilt-loader", + { ModPlatform::Quilt, { "net.minecraftforge", "net.neoforged", "net.fabricmc.fabric-loader" } } }, + { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } +}; + +Component::Component(PackProfile* parent, const QString& uid) +{ + assert(parent); + m_parent = parent; + + m_uid = uid; +} + +Component::Component(PackProfile* parent, const QString& uid, std::shared_ptr file) +{ + assert(parent); + m_parent = parent; + + m_file = file; + m_uid = uid; + m_cachedVersion = m_file->version; + m_cachedName = m_file->name; + m_loaded = true; +} + +std::shared_ptr Component::getMeta() +{ + return m_metaVersion; +} + +void Component::applyTo(LaunchProfile* profile) +{ + // do not apply disabled components + if (!isEnabled()) + { + return; + } + auto vfile = getVersionFile(); + if (vfile) + { + vfile->applyTo(profile, m_parent->runtimeContext()); + } + else + { + profile->applyProblemSeverity(getProblemSeverity()); + } +} + +std::shared_ptr Component::getVersionFile() const +{ + if (m_metaVersion) + { + return m_metaVersion->detailedData(); + } + else + { + return m_file; + } +} + +std::shared_ptr Component::getVersionList() const +{ + // Return nullptr if metadata index isn't loaded yet - caller should handle this case + auto index = APPLICATION->metadataIndex(); + if (!index || !index->hasComponent(m_uid)) + { + return nullptr; + } + return index->component(m_uid); +} + +int Component::getOrder() +{ + if (m_orderOverride) + return m_order; + + auto vfile = getVersionFile(); + if (vfile) + { + return vfile->order; + } + return 0; +} + +void Component::setOrder(int order) +{ + m_orderOverride = true; + m_order = order; +} + +QString Component::getID() +{ + return m_uid; +} + +QString Component::getName() +{ + if (!m_cachedName.isEmpty()) + return m_cachedName; + return m_uid; +} + +QString Component::getVersion() +{ + return m_cachedVersion; +} + +QString Component::getFilename() +{ + return m_parent->patchFilePathForUid(m_uid); +} + +QDateTime Component::getReleaseDateTime() +{ + if (m_metaVersion) + { + return m_metaVersion->releaseTime(); + } + auto vfile = getVersionFile(); + if (vfile) + { + return vfile->releaseTime; + } + // Fallback: return current time when no release time is available (e.g., custom components) + return QDateTime::currentDateTime(); +} + +bool Component::isEnabled() +{ + return !canBeDisabled() || !m_disabled; +} + +bool Component::canBeDisabled() +{ + return isRemovable() && !m_dependencyOnly; +} + +bool Component::setEnabled(bool state) +{ + bool intendedDisabled = !state; + if (!canBeDisabled()) + { + intendedDisabled = false; + } + if (intendedDisabled != m_disabled) + { + m_disabled = intendedDisabled; + emit dataChanged(); + return true; + } + return false; +} + +bool Component::isCustom() +{ + return m_file != nullptr; +} + +bool Component::isCustomizable() +{ + return m_metaVersion && getVersionFile(); +} + +bool Component::isRemovable() +{ + return !m_important; +} + +bool Component::isRevertible() +{ + if (isCustom()) + { + if (APPLICATION->metadataIndex()->hasComponent(m_uid)) + { + return true; + } + } + return false; +} + +bool Component::isMoveable() +{ + // Important components (like Minecraft) should stay at their fixed position + // Non-important components can be reordered by the user + return !m_important; +} + +bool Component::isVersionChangeable(bool wait) +{ + auto list = getVersionList(); + if (list) + { + if (wait) + list->waitUntilReady(); + return list->count() != 0; + } + return false; +} + +bool Component::isKnownModloader() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + return iter != KNOWN_MODLOADERS.cend(); +} + +QStringList Component::knownConflictingComponents() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + if (iter != KNOWN_MODLOADERS.cend()) + { + return (*iter).knownConflictingComponents; + } + else + { + return {}; + } +} + +void Component::setImportant(bool state) +{ + if (m_important != state) + { + m_important = state; + emit dataChanged(); + } +} + +ProblemSeverity Component::getProblemSeverity() const +{ + auto file = getVersionFile(); + if (file) + { + auto severity = file->getProblemSeverity(); + return m_componentProblemSeverity > severity ? m_componentProblemSeverity : severity; + } + return ProblemSeverity::Error; +} + +const QList Component::getProblems() const +{ + auto file = getVersionFile(); + if (file) + { + auto problems = file->getProblems(); + problems.append(m_componentProblems); + return problems; + } + return { { ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.") } }; +} + +void Component::addComponentProblem(ProblemSeverity severity, const QString& description) +{ + if (severity > m_componentProblemSeverity) + { + m_componentProblemSeverity = severity; + } + m_componentProblems.append({ severity, description }); + + emit dataChanged(); +} + +void Component::resetComponentProblems() +{ + m_componentProblems.clear(); + m_componentProblemSeverity = ProblemSeverity::None; + + emit dataChanged(); +} + +void Component::setVersion(const QString& version) +{ + if (version == m_version) + { + return; + } + m_version = version; + if (m_loaded) + { + // we are loaded and potentially have state to invalidate + if (m_file) + { + // we have a file... explicit version has been changed and there is nothing else to do. + } + else + { + // we don't have a file, therefore we are loaded with metadata + m_cachedVersion = version; + // see if the meta version is loaded + auto metaVersion = APPLICATION->metadataIndex()->version(m_uid, version); + if (metaVersion->isFullyLoaded()) + { + // if yes, we can continue with that. + m_metaVersion = metaVersion; + } + else + { + // if not, we need loading + m_metaVersion.reset(); + m_loaded = false; + } + updateCachedData(); + } + } + else + { + // not loaded... assume it will be sorted out later by the update task + } + emit dataChanged(); +} + +bool Component::customize() +{ + if (isCustom()) + { + return false; + } + + auto filename = getFilename(); + if (!FS::ensureFilePathExists(filename)) + { + return false; + } + // Using try-catch to handle potential JSON serialization errors gracefully + try + { + QSaveFile jsonFile(filename); + if (!jsonFile.open(QIODevice::WriteOnly)) + { + return false; + } + auto vfile = getVersionFile(); + if (!vfile) + { + return false; + } + auto document = OneSixVersionFormat::versionFileToJson(vfile); + jsonFile.write(document.toJson()); + if (!jsonFile.commit()) + { + return false; + } + m_file = vfile; + m_metaVersion.reset(); + emit dataChanged(); + } + catch (const Exception& error) + { + qWarning() << "Version could not be loaded:" << error.cause(); + } + return true; +} + +bool Component::revert() +{ + if (!isCustom()) + { + // already not custom + return true; + } + auto filename = getFilename(); + bool result = true; + // just kill the file and reload + if (QFile::exists(filename)) + { + result = FS::deletePath(filename); + } + if (result) + { + // file gone... + m_file.reset(); + + // check local cache for metadata... + auto version = APPLICATION->metadataIndex()->version(m_uid, m_version); + if (version->isFullyLoaded()) + { + m_metaVersion = version; + } + else + { + m_metaVersion.reset(); + m_loaded = false; + } + emit dataChanged(); + } + return result; +} + +/** + * deep inspecting compare for requirement sets + * By default, only uids are compared for set operations. + * This compares all fields of the Require structs in the sets. + */ +static bool deepCompare(const projt::meta::DependencySet& a, const projt::meta::DependencySet& b) +{ + // NOTE: this needs to be rewritten if the type of DependencySet changes + if (a.size() != b.size()) + { + return false; + } + for (const auto& reqA : a) + { + const auto& iter2 = b.find(reqA); + if (iter2 == b.cend()) + { + return false; + } + const auto& reqB = *iter2; + if (!reqA.deepEquals(reqB)) + { + return false; + } + } + return true; +} + +void Component::updateCachedData() +{ + auto file = getVersionFile(); + if (file) + { + bool changed = false; + if (m_cachedName != file->name) + { + m_cachedName = file->name; + changed = true; + } + if (m_cachedVersion != file->version) + { + m_cachedVersion = file->version; + changed = true; + } + if (m_cachedVolatile != file->m_volatile) + { + m_cachedVolatile = file->m_volatile; + changed = true; + } + if (!deepCompare(m_cachedRequires, file->m_requires)) + { + m_cachedRequires = file->m_requires; + changed = true; + } + if (!deepCompare(m_cachedConflicts, file->conflicts)) + { + m_cachedConflicts = file->conflicts; + changed = true; + } + if (changed) + { + emit dataChanged(); + } + } + else + { + // in case we removed all the metadata + m_cachedRequires.clear(); + m_cachedConflicts.clear(); + emit dataChanged(); + } +} + +void Component::waitLoadMeta() +{ + if (!m_loaded) + { + if (!m_metaVersion || !m_metaVersion->isFullyLoaded()) + { + // wait for the loaded version from meta + m_metaVersion = APPLICATION->metadataIndex()->loadVersionBlocking(m_uid, m_version); + } + m_loaded = true; + updateCachedData(); + } +} + +void Component::setUpdateAction(const UpdateAction& action) +{ + m_updateAction = action; +} + +UpdateAction Component::getUpdateAction() +{ + return m_updateAction; +} + +void Component::clearUpdateAction() +{ + m_updateAction = UpdateAction{ UpdateActionNone{} }; +} + +QDebug operator<<(QDebug d, const Component& comp) +{ + d << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; + return d; +} diff --git a/archived/projt-launcher/launcher/minecraft/Component.h b/archived/projt-launcher/launcher/minecraft/Component.h new file mode 100644 index 0000000000..6e92f84f20 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Component.h @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "ProblemProvider.h" +#include "QObjectPtr.h" +#include "meta/JsonFormat.hpp" +#include "modplatform/ModIndex.h" + +class PackProfile; +class LaunchProfile; +namespace projt::meta +{ + class MetaVersion; + class MetaVersionList; +} // namespace projt::meta +class VersionFile; + +struct UpdateActionChangeVersion +{ + /// version to change to + QString targetVersion; +}; +struct UpdateActionLatestRecommendedCompatible +{ + /// Parent uid + QString parentUid; + QString parentName; + /// Parent version + QString version; + /// +}; +struct UpdateActionRemove +{}; +struct UpdateActionImportantChanged +{ + QString oldVersion; +}; + +using UpdateActionNone = std::monostate; + +using UpdateAction = std::variant; + +struct ModloaderMapEntry +{ + ModPlatform::ModLoaderType type; + QStringList knownConflictingComponents; +}; + +class Component : public QObject, public ProblemProvider +{ + Q_OBJECT + public: + Component(PackProfile* parent, const QString& uid); + + // DEPRECATED: remove these constructors? + Component(PackProfile* parent, const QString& uid, std::shared_ptr file); + + virtual ~Component() + {} + + static const QMap KNOWN_MODLOADERS; + + void applyTo(LaunchProfile* profile); + + bool isEnabled(); + bool setEnabled(bool state); + bool canBeDisabled(); + + bool isMoveable(); + bool isCustomizable(); + bool isRevertible(); + bool isRemovable(); + bool isCustom(); + bool isVersionChangeable(bool wait = true); + bool isKnownModloader(); + QStringList knownConflictingComponents(); + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. + // NOTE: Kept for legacy migration support. + void setOrder(int order); + int getOrder(); + + QString getID(); + QString getName(); + QString getVersion(); + std::shared_ptr getMeta(); + QDateTime getReleaseDateTime(); + + QString getFilename(); + + std::shared_ptr getVersionFile() const; + std::shared_ptr getVersionList() const; + + void setImportant(bool state); + + const QList getProblems() const override; + ProblemSeverity getProblemSeverity() const override; + void addComponentProblem(ProblemSeverity severity, const QString& description); + void resetComponentProblems(); + + void setVersion(const QString& version); + bool customize(); + bool revert(); + + void updateCachedData(); + + void waitLoadMeta(); + + void setUpdateAction(const UpdateAction& action); + void clearUpdateAction(); + UpdateAction getUpdateAction(); + + signals: + void dataChanged(); + + public: /* data */ + PackProfile* m_parent; + + // BEGIN: persistent component list properties + /// ID of the component + QString m_uid; + /// version of the component - when there's a custom json override, this is also the version the component reverts to + QString m_version; + /// if true, this has been added automatically to satisfy dependencies and may be automatically removed + bool m_dependencyOnly = false; + /// if true, the component is either the main component of the instance, or otherwise important and cannot be removed. + bool m_important = false; + /// if true, the component is disabled + bool m_disabled = false; + + /// cached name for display purposes, taken from the version file (meta or local override) + QString m_cachedName; + /// cached version for display AND other purposes, taken from the version file (meta or local override) + QString m_cachedVersion; + /// cached set of requirements, taken from the version file (meta or local override) + projt::meta::DependencySet m_cachedRequires; + projt::meta::DependencySet m_cachedConflicts; + /// if true, the component is volatile and may be automatically removed when no longer needed + bool m_cachedVolatile = false; + // END: persistent component list properties + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. + // NOTE: Kept for legacy migration support. + bool m_orderOverride = false; + int m_order = 0; + + // load state + std::shared_ptr m_metaVersion; + std::shared_ptr m_file; + bool m_loaded = false; + + private: + QList m_componentProblems; + ProblemSeverity m_componentProblemSeverity = ProblemSeverity::None; + UpdateAction m_updateAction = UpdateAction{ UpdateActionNone{} }; +}; + +using ComponentPtr = shared_qobject_ptr; diff --git a/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.cpp b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.cpp new file mode 100644 index 0000000000..a1939f22d2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.cpp @@ -0,0 +1,1114 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ComponentUpdateTask.h" +#include + +#include "Component.h" +#include "ComponentUpdateTask_p.h" +#include "PackProfile.h" +#include "PackProfile_p.h" +#include "ProblemProvider.h" +#include "Version.h" +#include "cassert" +#include "meta/Index.hpp" +#include "meta/Version.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/ProfileUtils.h" +#include "net/Mode.h" + +#include "Application.h" +#include "tasks/Task.h" + +#include "minecraft/Logging.h" +#include "meta/VersionList.hpp" +#include "tasks/SequentialTask.h" + +/* + * This is responsible for loading the components of a component list AND resolving dependency issues between them + */ + +/* + * NOTE: The 'one shot async task' nature of this implementation is a known limitation. + * Ideally, this should be refactored into a reactor/state machine that receives input + * from the application and dynamically adapts to changing requirements. + * + * The reactor should be the only entry point for manipulating the PackProfile. + * See: https://en.wikipedia.org/wiki/Reactor_pattern + * + * Current implementation logic: + * - Operates on a snapshot of the PackProfile state. + * - Merges results as long as the snapshot and PackProfile haven't diverged during execution. + * - Requires a restart if the component list changes mid-operation. + */ + +/* + * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and + * PackProfile didn't change? If the component list changes, start over. + */ + +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() +{ + d.reset(new ComponentUpdateTaskData); + d->m_profile = list; + d->mode = mode; + d->netmode = netmode; +} + +ComponentUpdateTask::~ComponentUpdateTask() +{} + +void ComponentUpdateTask::executeTask() +{ + qCDebug(instanceProfileResolveC) << "Loading components"; + loadComponents(); +} + +namespace +{ + enum class LoadResult + { + LoadedLocal, + RequiresRemote, + Failed + }; + + LoadResult composeLoadResult(LoadResult a, LoadResult b) + { + if (a < b) + { + return b; + } + return a; + } + + static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) + { + if (component->m_loaded) + { + qCDebug(instanceProfileResolveC) << component->getName() << "is already loaded"; + return LoadResult::LoadedLocal; + } + + LoadResult result = LoadResult::Failed; + auto customPatchFilename = component->getFilename(); + if (QFile::exists(customPatchFilename)) + { + // if local file exists... + + // check for uid problems inside... + bool fileChanged = false; + auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false); + if (file->uid != component->m_uid) + { + file->uid = component->m_uid; + fileChanged = true; + } + if (fileChanged) + { + // Ensure we don't ignore failures when writing back a fixed json file. + bool saved = + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename); + if (!saved) + { + qCWarning(instanceProfileResolveC) + << "Failed to save modified component file:" << customPatchFilename; + } + } + + component->m_file = file; + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + auto metaVersion = APPLICATION->metadataIndex()->version(component->m_uid, component->m_version); + component->m_metaVersion = metaVersion; + if (metaVersion->isFullyLoaded()) + { + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + loadTask = + APPLICATION->metadataIndex()->loadVersionTask(component->m_uid, component->m_version, netmode); + loadTask->start(); + if (netmode == Net::Mode::Online) + result = LoadResult::RequiresRemote; + else if (metaVersion->isFullyLoaded()) + result = LoadResult::LoadedLocal; + else + result = LoadResult::Failed; + } + } + return result; + } + + static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) + { + auto index = APPLICATION->metadataIndex(); + + // If index is not yet synchronized and we're online, we need to load it first + if (index->state() != projt::meta::MetaEntity::State::Synchronized) + { + if (netmode == Net::Mode::Offline) + { + qCWarning(instanceProfileResolveC) << "Metadata index not available offline for" << component->m_uid; + return LoadResult::Failed; + } + + // Create a sequential task that first loads the index, then the version list + auto seq = makeShared( + ComponentUpdateTask::tr("Loading metadata for %1").arg(component->getName())); + seq->addTask(index->createLoadTask(netmode)); + + // After index loads, we need to load the version list + // Use a callback-based approach by connecting after index load completes + auto indexLoadTask = index->createLoadTask(netmode); + + // Create a task that will load version list after index is ready + class DeferredVersionListLoader : public Task + { + public: + DeferredVersionListLoader(ComponentPtr comp, Net::Mode mode) : m_component(comp), m_mode(mode) + {} + + void executeTask() override + { + auto versionList = m_component->getVersionList(); + if (!versionList) + { + emitFailed(tr("Component %1 not found in metadata index").arg(m_component->m_uid)); + return; + } + if (versionList->isLoaded()) + { + emitSucceeded(); + return; + } + m_innerTask = versionList->createLoadTask(m_mode); + connect(m_innerTask.get(), &Task::succeeded, this, [this]() { emitSucceeded(); }); + connect(m_innerTask.get(), + &Task::failed, + this, + [this](const QString& reason) { emitFailed(reason); }); + connect(m_innerTask.get(), &Task::progress, this, &Task::setProgress); + connect(m_innerTask.get(), &Task::status, this, &Task::setStatus); + m_innerTask->start(); + } + + bool canAbort() const override + { + return m_innerTask ? m_innerTask->canAbort() : false; + } + bool abort() override + { + return m_innerTask ? m_innerTask->abort() : Task::abort(); + } + + private: + ComponentPtr m_component; + Net::Mode m_mode; + Task::Ptr m_innerTask; + }; + + seq->addTask(makeShared(component, netmode)); + loadTask = seq; + loadTask->start(); + return LoadResult::RequiresRemote; + } + + // Index is already synchronized, get version list directly + auto versionList = component->getVersionList(); + if (!versionList) + { + qCWarning(instanceProfileResolveC) << "No version list found for" << component->m_uid; + return LoadResult::Failed; + } + + if (versionList->isLoaded()) + { + return LoadResult::LoadedLocal; + } + + if (netmode == Net::Mode::Offline) + { + return LoadResult::Failed; + } + + loadTask = versionList->createLoadTask(netmode); + + if (loadTask) + { + loadTask->start(); + return LoadResult::RequiresRemote; + } + + return LoadResult::Failed; + } + +} // namespace + +void ComponentUpdateTask::loadComponents() +{ + LoadResult result = LoadResult::LoadedLocal; + size_t taskIndex = 0; + size_t componentIndex = 0; + d->remoteLoadSuccessful = true; + + // load all the components OR their lists... + for (auto component : d->m_profile->d->components) + { + Task::Ptr loadTask; + LoadResult singleResult = LoadResult::LoadedLocal; + RemoteLoadStatus::Type loadType; + component->resetComponentProblems(); + // Load version lists or components based on resolution mode + switch (d->mode) + { + case Mode::Launch: + { + singleResult = loadComponent(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::Version; + break; + } + case Mode::Resolution: + { + singleResult = loadPackProfile(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::List; + break; + } + } + if (singleResult == LoadResult::LoadedLocal) + { + component->updateCachedData(); + } + result = composeLoadResult(result, singleResult); + if (loadTask) + { + qCDebug(instanceProfileResolveC) << d->m_profile->d->m_instance->name() << "|" + << "Remote loading is being run for" << component->getName(); + connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); }); + connect(loadTask.get(), + &Task::failed, + this, + [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); }); + connect(loadTask.get(), + &Task::aborted, + this, + [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); + RemoteLoadStatus status; + status.type = loadType; + status.PackProfileIndex = componentIndex; + status.task = loadTask; + d->remoteLoadStatusList.append(status); + taskIndex++; + } + componentIndex++; + } + d->remoteTasksInProgress = taskIndex; + switch (result) + { + case LoadResult::LoadedLocal: + { + // Everything got loaded. Advance to dependency resolution. + performUpdateActions(); + // In offline mode, only check dependencies; in online mode, resolve them + resolveDependencies(d->netmode == Net::Mode::Offline); + break; + } + case LoadResult::RequiresRemote: + { + // we wait for signals. + break; + } + case LoadResult::Failed: + { + emitFailed(tr("Some component metadata load tasks failed.")); + break; + } + } +} + +namespace +{ + struct RequireEx : public projt::meta::ComponentDependency + { + size_t indexOfFirstDependee = 0; + }; + struct RequireCompositionResult + { + bool ok; + RequireEx outcome; + }; + using RequireExSet = std::set; +} // namespace + +static RequireCompositionResult composeRequirement(const RequireEx& a, const RequireEx& b) +{ + assert(a.uid == b.uid); + RequireEx out; + out.uid = a.uid; + out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee); + if (a.equalsVersion.isEmpty()) + { + out.equalsVersion = b.equalsVersion; + } + else if (b.equalsVersion.isEmpty()) + { + out.equalsVersion = a.equalsVersion; + } + else if (a.equalsVersion == b.equalsVersion) + { + out.equalsVersion = a.equalsVersion; + } + else + { + // Version conflict: different exact versions required for same component + qWarning() << "Version conflict for" << a.uid << ":" << a.equalsVersion << "vs" << b.equalsVersion; + return { false, out }; + } + + if (a.suggests.isEmpty()) + { + out.suggests = b.suggests; + } + else if (b.suggests.isEmpty()) + { + out.suggests = a.suggests; + } + else + { + Version aVer(a.suggests); + Version bVer(b.suggests); + out.suggests = (aVer < bVer ? b.suggests : a.suggests); + } + return { true, out }; +} + +// gather the requirements from all components, finding any obvious conflicts +static bool gatherRequirementsFromComponents(const ComponentContainer& input, RequireExSet& output) +{ + bool succeeded = true; + size_t componentNum = 0; + for (auto component : input) + { + auto& componentRequires = component->m_cachedRequires; + for (const auto& componentRequire : componentRequires) + { + auto found = std::find_if(output.cbegin(), + output.cend(), + [componentRequire](const projt::meta::ComponentDependency& req) + { return req.uid == componentRequire.uid; }); + + RequireEx componenRequireEx; + componenRequireEx.uid = componentRequire.uid; + componenRequireEx.suggests = componentRequire.suggests; + componenRequireEx.equalsVersion = componentRequire.equalsVersion; + componenRequireEx.indexOfFirstDependee = componentNum; + + if (found != output.cend()) + { + // found... process it further + auto result = composeRequirement(componenRequireEx, *found); + if (result.ok) + { + output.erase(componenRequireEx); + output.insert(result.outcome); + } + else + { + qCCritical(instanceProfileResolveC) + << "Conflicting requirements:" << componentRequire.uid + << "versions:" << componentRequire.equalsVersion << ";" << (*found).equalsVersion; + } + succeeded &= result.ok; + } + else + { + // not found, accumulate + output.insert(componenRequireEx); + } + } + componentNum++; + } + return succeeded; +} + +/// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps) +static void getTrivialRemovals(const ComponentContainer& components, const RequireExSet& reqs, QStringList& toRemove) +{ + for (const auto& component : components) + { + if (!component->m_dependencyOnly) + continue; + if (!component->m_cachedVolatile) + continue; + RequireEx reqNeedle; + reqNeedle.uid = component->m_uid; + const auto iter = reqs.find(reqNeedle); + if (iter == reqs.cend()) + { + toRemove.append(component->m_uid); + } + } +} + +/** + * handles: + * - trivial addition (there is an unmet requirement and it can be trivially met by adding something) + * - trivial version conflict of dependencies == explicit version required and installed is different + * + * toAdd - set of requirements than mean adding a new component + * toChange - set of requirements that mean changing version of an existing component + */ +static bool getTrivialComponentChanges(const ComponentIndex& index, + const RequireExSet& input, + RequireExSet& toAdd, + RequireExSet& toChange) +{ + enum class Decision + { + Undetermined, + Met, + Missing, + VersionNotSame, + LockedVersionNotSame + } decision = Decision::Undetermined; + + QString reqStr; + bool succeeded = true; + // list the composed requirements and say if they are met or unmet + for (auto& req : input) + { + do + { + if (req.equalsVersion.isEmpty()) + { + reqStr = QString("Req: %1").arg(req.uid); + if (index.contains(req.uid)) + { + decision = Decision::Met; + } + else + { + toAdd.insert(req); + decision = Decision::Missing; + } + break; + } + else + { + reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion); + const auto& compIter = index.find(req.uid); + if (compIter == index.cend()) + { + toAdd.insert(req); + decision = Decision::Missing; + break; + } + auto& comp = (*compIter); + if (comp->getVersion() != req.equalsVersion) + { + if (comp->isCustom()) + { + decision = Decision::LockedVersionNotSame; + } + else + { + if (comp->m_dependencyOnly) + { + decision = Decision::VersionNotSame; + } + else + { + decision = Decision::LockedVersionNotSame; + } + } + break; + } + decision = Decision::Met; + } + } + while (false); + switch (decision) + { + case Decision::Undetermined: + qCCritical(instanceProfileResolveC) << "No decision for" << reqStr; + succeeded = false; + break; + case Decision::Met: qCDebug(instanceProfileResolveC) << reqStr << "Is met."; break; + case Decision::Missing: + qCDebug(instanceProfileResolveC) + << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; + toAdd.insert(req); + break; + case Decision::VersionNotSame: + qCDebug(instanceProfileResolveC) << reqStr << "already has different version that can be changed."; + toChange.insert(req); + break; + case Decision::LockedVersionNotSame: + qCDebug(instanceProfileResolveC) << reqStr << "already has different version that cannot be changed."; + succeeded = false; + break; + } + } + return succeeded; +} + +ComponentContainer ComponentUpdateTask::collectTreeLinked(const QString& uid) +{ + ComponentContainer linked; + + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + auto& instance = d->m_profile->d->m_instance; + for (auto comp : components) + { + qCDebug(instanceProfileResolveC) << instance->name() << "|" + << "scanning" << comp->getID() << ":" << comp->getVersion() << "for tree link"; + auto dep = std::find_if(comp->m_cachedRequires.cbegin(), + comp->m_cachedRequires.cend(), + [uid](const projt::meta::ComponentDependency& req) -> bool { return req.uid == uid; }); + if (dep != comp->m_cachedRequires.cend()) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "depends on" << uid; + linked.append(comp); + } + } + auto iter = componentIndex.find(uid); + if (iter != componentIndex.end()) + { + ComponentPtr comp = *iter; + comp->updateCachedData(); + qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() + << "has" << comp->m_cachedRequires.size() << "dependencies"; + for (auto dep : comp->m_cachedRequires) + { + qCDebug(instanceProfileC) << instance->name() << "|" << uid << "depends on" << dep.uid; + auto found = componentIndex.find(dep.uid); + if (found != componentIndex.end()) + { + qCDebug(instanceProfileC) << instance->name() << "|" << (*found)->getID() << "is present"; + linked.append(*found); + } + } + } + return linked; +} + +// Architecture Note: This method directly manipulates PackProfile internals. +// Proper abstraction would require richer data types (dependency graph, version constraints), +// but current implementation is sufficient for the launcher's use case. +// A full rewrite to use a proper dependency graph (like SAT solving) is out of scope for now. +void ComponentUpdateTask::resolveDependencies(bool checkOnly) +{ + qCDebug(instanceProfileResolveC) << "Resolving dependencies"; + /* + * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple + * ways: + * 1. There are conflicting dependencies on the same uid with different exact version numbers + * -> hard error + * 2. A dependency has non-matching exact version number + * -> hard error + * 3. A dependency is entirely missing and needs to be injected before the dependee(s) + * -> requirements are injected + * + * NOTE: this is a placeholder and should eventually be replaced with something 'serious' + */ + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + + RequireExSet allRequires; + QStringList toRemove; + do + { + allRequires.clear(); + toRemove.clear(); + if (!gatherRequirementsFromComponents(components, allRequires)) + { + finalizeComponents(); + emitFailed(tr("Conflicting requirements detected during dependency checking!")); + return; + } + getTrivialRemovals(components, allRequires, toRemove); + if (!toRemove.isEmpty()) + { + qCDebug(instanceProfileResolveC) << "Removing obsolete components..."; + for (auto& remove : toRemove) + { + qCDebug(instanceProfileResolveC) << "Removing" << remove; + d->m_profile->remove(remove); + } + } + } + while (!toRemove.isEmpty()); + RequireExSet toAdd; + RequireExSet toChange; + bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); + if (!succeeded) + { + finalizeComponents(); + emitFailed(tr("Instance has conflicting dependencies.")); + return; + } + if (checkOnly) + { + finalizeComponents(); + if (toAdd.size() || toChange.size()) + { + emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); + } + else + { + emitSucceeded(); + } + return; + } + + bool recursionNeeded = false; + if (toAdd.size()) + { + // add stuff... + for (auto& add : toAdd) + { + auto component = makeShared(d->m_profile, add.uid); + if (!add.equalsVersion.isEmpty()) + { + // exact version + qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "version" << add.equalsVersion + << "at position" << add.indexOfFirstDependee; + component->m_version = add.equalsVersion; + } + else + { + // version needs to be decided + qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; + if (!add.suggests.isEmpty()) + { + // Use suggested version if available + component->m_version = add.suggests; + } + else + { + // Intermediary mappings must align with the selected Minecraft version. + // Prefer a metadata entry that explicitly depends on that MC version. + if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.hashed") + { + auto minecraft = + std::find_if(components.begin(), + components.end(), + [](ComponentPtr& cmp) { return cmp->getID() == "net.minecraft"; }); + if (minecraft != components.end()) + { + const auto minecraftVersion = (*minecraft)->getVersion(); + auto versionList = APPLICATION->metadataIndex()->component(add.uid); + if (versionList) + { + versionList->waitUntilReady(); + auto matched = versionList->stableForParent("net.minecraft", minecraftVersion); + if (!matched) + { + matched = versionList->latestForParent("net.minecraft", minecraftVersion); + } + if (matched) + { + component->m_version = matched->descriptor(); + } + } + if (component->m_version.isEmpty()) + { + component->m_version = minecraftVersion; + } + } + } + + // Try to get recommended version from metadata + if (component->m_version.isEmpty()) + { + auto versionList = APPLICATION->metadataIndex()->component(add.uid); + if (versionList) + { + versionList->waitUntilReady(); + auto recommended = versionList->getRecommended(); + if (recommended) + { + component->m_version = recommended->descriptor(); + } + } + } + + // Last resort: known defaults for LWJGL when metadata unavailable + if (component->m_version.isEmpty()) + { + if (add.uid == "org.lwjgl") + { + component->m_version = "2.9.1"; + } + else if (add.uid == "org.lwjgl3") + { + component->m_version = "3.1.2"; + } + } + } + } + component->m_dependencyOnly = true; + // Direct insertion to component list is intentional - this is part of dependency resolution + // which requires atomic updates to the profile structure. + d->m_profile->insertComponent(add.indexOfFirstDependee, component); + componentIndex[add.uid] = component; + } + recursionNeeded = true; + } + if (toChange.size()) + { + // Version changes during dependency resolution require direct component access + // as the dependency resolver may adjust versions to satisfy constraints. + for (auto& change : toChange) + { + qCDebug(instanceProfileResolveC) << "Setting version of " << change.uid << "to" << change.equalsVersion; + auto component = componentIndex[change.uid]; + component->setVersion(change.equalsVersion); + } + recursionNeeded = true; + } + + if (recursionNeeded) + { + loadComponents(); + } + else + { + finalizeComponents(); + emitSucceeded(); + } +} + +// Variant visitation via lambda +template +struct overload : Ts... +{ + using Ts::operator()...; +}; +template +overload(Ts...) -> overload; + +void ComponentUpdateTask::performUpdateActions() +{ + auto& instance = d->m_profile->d->m_instance; + bool addedActions; + QStringList toRemove; + do + { + addedActions = false; + toRemove.clear(); + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + for (auto component : components) + { + if (!component) + { + continue; + } + auto action = component->getUpdateAction(); + auto visitor = overload{ + [](const UpdateActionNone&) + { + // noop + }, + [&component, &instance](const UpdateActionChangeVersion& cv) + { + qCDebug(instanceProfileResolveC) << instance->name() << "|" + << "UpdateActionChangeVersion" << component->getID() << ":" + << component->getVersion() << "change to" << cv.targetVersion; + component->setVersion(cv.targetVersion); + component->waitLoadMeta(); + }, + [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" + << component->getVersion() << "updating to latest recommend or compatible with" << lrc.parentUid + << lrc.version; + auto versionList = APPLICATION->metadataIndex()->component(component->getID()); + if (versionList) + { + versionList->waitUntilReady(); + auto recommended = versionList->stableForParent(lrc.parentUid, lrc.version); + if (!recommended) + { + recommended = versionList->latestForParent(lrc.parentUid, lrc.version); + } + if (recommended) + { + component->setVersion(recommended->versionId()); + component->waitLoadMeta(); + return; + } + else + { + component->addComponentProblem(ProblemSeverity::Error, + QObject::tr("No compatible version of %1 found for %2 %3") + .arg(component->getName(), lrc.parentName, lrc.version)); + } + } + else + { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("No version list in metadata index for %1").arg(component->getID())); + } + }, + [&component, &instance, &toRemove](const UpdateActionRemove&) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateActionRemove" << component->getID() << ":" << component->getVersion() << "removing"; + toRemove.append(component->getID()); + }, + [this, &component, &instance, &addedActions, &componentIndex](const UpdateActionImportantChanged& ic) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateImportantChanged" << component->getID() << ":" << component->getVersion() + << "was changed from" << ic.oldVersion << "updating linked components"; + auto oldVersion = + APPLICATION->metadataIndex()->loadVersionBlocking(component->getID(), ic.oldVersion); + for (auto oldReq : oldVersion->dependencies()) + { + auto currentlyRequired = component->m_cachedRequires.find(oldReq); + if (currentlyRequired == component->m_cachedRequires.cend()) + { + auto oldReqComp = componentIndex.find(oldReq.uid); + if (oldReqComp != componentIndex.cend()) + { + (*oldReqComp)->setUpdateAction(UpdateAction{ UpdateActionRemove{} }); + addedActions = true; + } + } + } + auto linked = collectTreeLinked(component->getID()); + for (auto comp : linked) + { + if (comp->isCustom()) + { + continue; + } + auto compUid = comp->getID(); + auto parentReq = std::find_if(component->m_cachedRequires.begin(), + component->m_cachedRequires.end(), + [compUid](const projt::meta::ComponentDependency& req) + { return req.uid == compUid; }); + if (parentReq != component->m_cachedRequires.end()) + { + auto newVersion = + parentReq->equalsVersion.isEmpty() ? parentReq->suggests : parentReq->equalsVersion; + if (!newVersion.isEmpty()) + { + comp->setUpdateAction(UpdateAction{ UpdateActionChangeVersion{ newVersion } }); + } + else + { + comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ + component->getID(), + component->getName(), + component->getVersion(), + } }); + } + } + else + { + comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ + component->getID(), + component->getName(), + component->getVersion(), + } }); + } + addedActions = true; + } + } + }; + std::visit(visitor, action); + component->clearUpdateAction(); + for (auto uid : toRemove) + { + d->m_profile->remove(uid); + } + } + } + while (addedActions); +} + +void ComponentUpdateTask::finalizeComponents() +{ + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + for (auto component : components) + { + for (auto req : component->m_cachedRequires) + { + auto found = componentIndex.find(req.uid); + if (found == componentIndex.cend()) + { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("%1 is missing requirement %2 %3") + .arg(component->getName(), + req.uid, + req.equalsVersion.isEmpty() ? req.suggests : req.equalsVersion)); + } + else + { + auto reqComp = *found; + if (!reqComp->getProblems().isEmpty()) + { + component->addComponentProblem( + reqComp->getProblemSeverity(), + QObject::tr("%1, a dependency of this component, has reported issues").arg(reqComp->getName())); + } + if (!req.equalsVersion.isEmpty() && req.equalsVersion != reqComp->getVersion()) + { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("%1, a dependency of this component, is not the required version %2") + .arg(reqComp->getName(), req.equalsVersion)); + } + else if (!req.suggests.isEmpty() && req.suggests != reqComp->getVersion()) + { + component->addComponentProblem( + ProblemSeverity::Warning, + QObject::tr("%1, a dependency of this component, is not the suggested version %2") + .arg(reqComp->getName(), req.suggests)); + } + } + } + for (auto conflict : component->knownConflictingComponents()) + { + auto found = componentIndex.find(conflict); + if (found != componentIndex.cend()) + { + auto foundComp = *found; + if (foundComp->isCustom()) + { + continue; + } + component->addComponentProblem( + ProblemSeverity::Warning, + QObject::tr("%1 and %2 are known to not work together. It is recommended to remove one of them.") + .arg(component->getName(), foundComp->getName())); + } + } + } +} + +void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) +{ + if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) + { + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; + return; + } + auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); + if (taskSlot.finished) + { + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; + return; + } + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "succeeded"; + taskSlot.succeeded = true; + taskSlot.finished = true; + d->remoteTasksInProgress--; + // update the cached data of the component from the downloaded version file. + if (taskSlot.type == RemoteLoadStatus::Type::Version) + { + auto component = d->m_profile->getComponent(taskSlot.PackProfileIndex); + component->m_loaded = true; + component->updateCachedData(); + } + checkIfAllFinished(); +} + +void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) +{ + if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) + { + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; + return; + } + auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); + if (taskSlot.finished) + { + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; + return; + } + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed: " << msg; + d->remoteLoadSuccessful = false; + taskSlot.succeeded = false; + taskSlot.finished = true; + d->remoteTasksInProgress--; + checkIfAllFinished(); +} + +void ComponentUpdateTask::checkIfAllFinished() +{ + if (d->remoteTasksInProgress) + { + // not yet... + return; + } + if (d->remoteLoadSuccessful) + { + // nothing bad happened... clear the temp load status and proceed with looking at dependencies + d->remoteLoadStatusList.clear(); + performUpdateActions(); + // In online mode, resolve dependencies (add missing components) + // In offline mode, only check (no network to download new components) + resolveDependencies(d->netmode == Net::Mode::Offline); + } + else + { + // remote load failed... report error and bail + QStringList allErrorsList; + for (auto& item : d->remoteLoadStatusList) + { + if (!item.succeeded) + { + const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex); + allErrorsList.append( + tr("Could not download metadata for %1 %2. Please change the version or try again later.") + .arg(component->getName(), component->m_version)); + } + } + auto allErrors = allErrorsList.join("\n"); + emitFailed( + tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); + d->remoteLoadStatusList.clear(); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.h b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.h new file mode 100644 index 0000000000..f0df31dc4d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "minecraft/Component.h" +#include "net/Mode.h" +#include "tasks/Task.h" + +#include +class PackProfile; +struct ComponentUpdateTaskData; + +class ComponentUpdateTask : public Task +{ + Q_OBJECT + public: + enum class Mode + { + Launch, + Resolution + }; + + public: + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); + virtual ~ComponentUpdateTask(); + + protected: + void executeTask() override; + + private: + void loadComponents(); + /// collects components that are dependent on or dependencies of the component + QList collectTreeLinked(const QString& uid); + void resolveDependencies(bool checkOnly); + void performUpdateActions(); + void finalizeComponents(); + + void remoteLoadSucceeded(size_t index); + void remoteLoadFailed(size_t index, const QString& msg); + void checkIfAllFinished(); + + private: + std::unique_ptr d; +}; diff --git a/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask_p.h b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask_p.h new file mode 100644 index 0000000000..bcb139055c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask_p.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include "net/Mode.h" +#include "tasks/Task.h" + +#include "minecraft/ComponentUpdateTask.h" + +class PackProfile; + +struct RemoteLoadStatus +{ + enum class Type + { + Index, + List, + Version + } type = Type::Version; + size_t PackProfileIndex = 0; + bool finished = false; + bool succeeded = false; + Task::Ptr task; +}; + +struct ComponentUpdateTaskData +{ + PackProfile* m_profile = nullptr; + QList remoteLoadStatusList; + bool remoteLoadSuccessful = true; + size_t remoteTasksInProgress = 0; + ComponentUpdateTask::Mode mode; + Net::Mode netmode; +}; diff --git a/archived/projt-launcher/launcher/minecraft/GradleSpecifier.h b/archived/projt-launcher/launcher/minecraft/GradleSpecifier.h new file mode 100644 index 0000000000..aeb3beda88 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/GradleSpecifier.h @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include "DefaultVariable.h" + +struct GradleSpecifier +{ + GradleSpecifier() + { + m_valid = false; + } + GradleSpecifier(QString value) + { + operator=(value); + } + GradleSpecifier& operator=(const QString& value) + { + /* + org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar + 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + 1 "org.gradle.test.classifiers" + 2 "service" + 3 "1.0" + 4 "jdk15" + 5 "jar" + */ + static const QRegularExpression s_matcher(QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" + "(?::([^:@]+))?" + "(?:@([^:@]+))?")); + QRegularExpressionMatch match = s_matcher.match(value); + m_valid = match.hasMatch(); + if (!m_valid) + { + m_invalidValue = value; + return *this; + } + auto elements = match.captured(); + m_groupId = match.captured(1); + m_artifactId = match.captured(2); + m_version = match.captured(3); + m_classifier = match.captured(4); + if (match.lastCapturedIndex() >= 5) + { + m_extension = match.captured(5); + } + return *this; + } + QString serialize() const + { + if (!m_valid) + { + return m_invalidValue; + } + QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; + if (!m_classifier.isEmpty()) + { + retval += ":" + m_classifier; + } + if (m_extension.isExplicit()) + { + retval += "@" + m_extension; + } + return retval; + } + QString getFileName() const + { + if (!m_valid) + { + return QString(); + } + QString filename = m_artifactId + '-' + m_version; + if (!m_classifier.isEmpty()) + { + filename += "-" + m_classifier; + } + filename += "." + m_extension; + return filename; + } + QString toPath(const QString& filenameOverride = QString()) const + { + if (!m_valid) + { + return QString(); + } + QString filename; + if (filenameOverride.isEmpty()) + { + filename = getFileName(); + } + else + { + filename = filenameOverride; + } + QString path = m_groupId; + path.replace('.', '/'); + path += '/' + m_artifactId + '/' + m_version + '/' + filename; + return path; + } + inline bool valid() const + { + return m_valid; + } + inline QString version() const + { + return m_version; + } + inline QString groupId() const + { + return m_groupId; + } + inline QString artifactId() const + { + return m_artifactId; + } + inline void setClassifier(const QString& classifier) + { + m_classifier = classifier; + } + inline QString classifier() const + { + return m_classifier; + } + inline QString extension() const + { + return m_extension; + } + inline QString artifactPrefix() const + { + return m_groupId + ":" + m_artifactId; + } + bool matchName(const GradleSpecifier& other) const + { + return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); + } + bool operator==(const GradleSpecifier& other) const + { + if (m_groupId != other.m_groupId) + return false; + if (m_artifactId != other.m_artifactId) + return false; + if (m_version != other.m_version) + return false; + if (m_classifier != other.m_classifier) + return false; + if (m_extension != other.m_extension) + return false; + return true; + } + + private: + QString m_invalidValue; + QString m_groupId; + QString m_artifactId; + QString m_version; + QString m_classifier; + DefaultVariable m_extension = DefaultVariable("jar"); + bool m_valid = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/LaunchProfile.cpp b/archived/projt-launcher/launcher/minecraft/LaunchProfile.cpp new file mode 100644 index 0000000000..5ad1ef19f3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/LaunchProfile.cpp @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "LaunchProfile.h" +#include + +void LaunchProfile::clear() +{ + m_minecraftVersion.clear(); + m_minecraftVersionType.clear(); + m_minecraftAssets.reset(); + m_minecraftArguments.clear(); + m_addnJvmArguments.clear(); + m_tweakers.clear(); + m_mainClass.clear(); + m_appletClass.clear(); + m_libraries.clear(); + m_mavenFiles.clear(); + m_agents.clear(); + m_traits.clear(); + m_jarMods.clear(); + m_mainJar.reset(); + m_problemSeverity = ProblemSeverity::None; +} + +static void applyString(const QString& from, QString& to) +{ + if (from.isEmpty()) + return; + to = from; +} + +void LaunchProfile::applyMinecraftVersion(const QString& id) +{ + applyString(id, this->m_minecraftVersion); +} + +void LaunchProfile::applyAppletClass(const QString& appletClass) +{ + applyString(appletClass, this->m_appletClass); +} + +void LaunchProfile::applyMainClass(const QString& mainClass) +{ + applyString(mainClass, this->m_mainClass); +} + +void LaunchProfile::applyMinecraftArguments(const QString& minecraftArguments) +{ + applyString(minecraftArguments, this->m_minecraftArguments); +} + +void LaunchProfile::applyAddnJvmArguments(const QStringList& addnJvmArguments) +{ + this->m_addnJvmArguments.append(addnJvmArguments); +} + +void LaunchProfile::applyMinecraftVersionType(const QString& type) +{ + applyString(type, this->m_minecraftVersionType); +} + +void LaunchProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) +{ + if (assets) + { + m_minecraftAssets = assets; + } +} + +void LaunchProfile::applyTraits(const QSet& traits) +{ + this->m_traits.unite(traits); +} + +void LaunchProfile::applyTweakers(const QStringList& tweakers) +{ + // if the applied tweakers override an existing one, skip it. this effectively moves it later in the sequence + QStringList newTweakers; + for (auto& tweaker : m_tweakers) + { + if (tweakers.contains(tweaker)) + { + continue; + } + newTweakers.append(tweaker); + } + // then just append the new tweakers (or moved original ones) + newTweakers += tweakers; + m_tweakers = newTweakers; +} + +void LaunchProfile::applyJarMods(const QList& jarMods) +{ + this->m_jarMods.append(jarMods); +} + +static int findLibraryByName(QList* haystack, const GradleSpecifier& needle) +{ + int retval = -1; + for (int i = 0; i < haystack->size(); ++i) + { + if (haystack->at(i)->rawName().matchName(needle)) + { + // only one is allowed. + if (retval != -1) + return -1; + retval = i; + } + } + return retval; +} + +void LaunchProfile::applyMods(const QList& mods) +{ + QList* list = &m_mods; + for (auto& mod : mods) + { + auto modCopy = Library::limitedCopy(mod); + + // find the mod by name. + const int index = findLibraryByName(list, mod->rawName()); + // mod not found? just add it. + if (index < 0) + { + list->append(modCopy); + return; + } + + auto existingLibrary = list->at(index); + // if we are higher it means we should update + if (Version(mod->version()) > Version(existingLibrary->version())) + { + list->replace(index, modCopy); + } + } +} + +void LaunchProfile::applyCompatibleJavaMajors(QList& javaMajor) +{ + m_compatibleJavaMajors.append(javaMajor); +} + +void LaunchProfile::applyCompatibleJavaName(QString javaName) +{ + if (!javaName.isEmpty()) + m_compatibleJavaName = javaName; +} + +void LaunchProfile::applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext) +{ + if (!library->isActive(runtimeContext)) + { + return; + } + + QList* list = &m_libraries; + if (library->isNative()) + { + list = &m_nativeLibraries; + } + + auto libraryCopy = Library::limitedCopy(library); + + // find the library by name. + const int index = findLibraryByName(list, library->rawName()); + // library not found? just add it. + if (index < 0) + { + list->append(libraryCopy); + return; + } + + auto existingLibrary = list->at(index); + // if we are higher it means we should update + if (Version(library->version()) > Version(existingLibrary->version())) + { + list->replace(index, libraryCopy); + } +} + +void LaunchProfile::applyMavenFile(LibraryPtr mavenFile, const RuntimeContext& runtimeContext) +{ + if (!mavenFile->isActive(runtimeContext)) + { + return; + } + + if (mavenFile->isNative()) + { + return; + } + + // unlike libraries, we do not keep only one version or try to dedupe them + m_mavenFiles.append(Library::limitedCopy(mavenFile)); +} + +void LaunchProfile::applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext) +{ + auto lib = agent->library(); + if (!lib->isActive(runtimeContext)) + { + return; + } + + if (lib->isNative()) + { + return; + } + + m_agents.append(agent); +} + +const LibraryPtr LaunchProfile::getMainJar() const +{ + return m_mainJar; +} + +void LaunchProfile::applyMainJar(LibraryPtr jar) +{ + if (jar) + { + m_mainJar = jar; + } +} + +void LaunchProfile::applyProblemSeverity(ProblemSeverity severity) +{ + if (m_problemSeverity < severity) + { + m_problemSeverity = severity; + } +} + +const QList LaunchProfile::getProblems() const +{ + QList problems; + + // Check for critical configuration issues + if (m_mainClass.isEmpty() && m_appletClass.isEmpty()) + { + problems.append({ ProblemSeverity::Error, QObject::tr("No main class or applet class specified") }); + } + + if (m_minecraftVersion.isEmpty()) + { + problems.append({ ProblemSeverity::Error, QObject::tr("Minecraft version is not specified") }); + } + + if (m_minecraftArguments.isEmpty() && m_minecraftVersionType != "snapshot" && m_minecraftVersionType != "old_alpha") + { + problems.append({ ProblemSeverity::Warning, QObject::tr("No game arguments specified (may be intentional)") }); + } + + // Check for missing main jar + if (!m_mainJar) + { + problems.append({ ProblemSeverity::Error, QObject::tr("Main jar file is missing") }); + } + + // Check if there are any libraries at all + if (m_minecraftArguments.isEmpty() && m_minecraftVersionType != "snapshot" && m_minecraftVersionType != "old_alpha") + { + problems.append( + { ProblemSeverity::Warning, QObject::tr("No libraries specified (unusual for modern Minecraft)") }); + } + + return problems; +} + +QString LaunchProfile::getMinecraftVersion() const +{ + return m_minecraftVersion; +} + +QString LaunchProfile::getAppletClass() const +{ + return m_appletClass; +} + +QString LaunchProfile::getMainClass() const +{ + return m_mainClass; +} + +const QSet& LaunchProfile::getTraits() const +{ + return m_traits; +} + +const QStringList& LaunchProfile::getTweakers() const +{ + return m_tweakers; +} + +bool LaunchProfile::hasTrait(const QString& trait) const +{ + return m_traits.contains(trait); +} + +ProblemSeverity LaunchProfile::getProblemSeverity() const +{ + return m_problemSeverity; +} + +QString LaunchProfile::getMinecraftVersionType() const +{ + return m_minecraftVersionType; +} + +std::shared_ptr LaunchProfile::getMinecraftAssets() const +{ + if (!m_minecraftAssets) + { + return std::make_shared("legacy"); + } + return m_minecraftAssets; +} + +QString LaunchProfile::getMinecraftArguments() const +{ + return m_minecraftArguments; +} + +const QStringList& LaunchProfile::getAddnJvmArguments() const +{ + return m_addnJvmArguments; +} + +const QList& LaunchProfile::getJarMods() const +{ + return m_jarMods; +} + +const QList& LaunchProfile::getLibraries() const +{ + return m_libraries; +} + +const QList& LaunchProfile::getNativeLibraries() const +{ + return m_nativeLibraries; +} + +const QList& LaunchProfile::getMavenFiles() const +{ + return m_mavenFiles; +} + +const QList& LaunchProfile::getAgents() const +{ + return m_agents; +} + +const QList& LaunchProfile::getCompatibleJavaMajors() const +{ + return m_compatibleJavaMajors; +} + +const QString LaunchProfile::getCompatibleJavaName() const +{ + return m_compatibleJavaName; +} + +void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, + QStringList& jars, + QStringList& nativeJars, + const QString& overridePath, + const QString& moddedJarSearchResultPath) const +{ + QStringList native32, native64; + jars.clear(); + nativeJars.clear(); + for (auto lib : getLibraries()) + { + lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); + } + // NOTE: order is important here, add main jar last to the lists + if (m_mainJar) + { + // NOTE: If we have jar mods, we use the modified jar allocated by ModMinecraftJar step + // The modified jar is expected to be named "minecraft.jar" in the search path. + if (m_jarMods.size() && !moddedJarSearchResultPath.isEmpty()) + { + QDir moddedJarDir(moddedJarSearchResultPath); + jars.append(moddedJarDir.absoluteFilePath("minecraft.jar")); + } + else + { + m_mainJar->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); + } + } + for (auto lib : getNativeLibraries()) + { + lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); + } + if (runtimeContext.javaArchitecture == "32") + { + nativeJars.append(native32); + } + else if (runtimeContext.javaArchitecture == "64") + { + nativeJars.append(native64); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/LaunchProfile.h b/archived/projt-launcher/launcher/minecraft/LaunchProfile.h new file mode 100644 index 0000000000..f281e8797f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/LaunchProfile.h @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once +#include +#include +#include "Agent.h" +#include "Library.h" + +class LaunchProfile : public ProblemProvider +{ + public: + virtual ~LaunchProfile() + {} + + public: /* application of profile variables from patches */ + void applyMinecraftVersion(const QString& id); + void applyMainClass(const QString& mainClass); + void applyAppletClass(const QString& appletClass); + void applyMinecraftArguments(const QString& minecraftArguments); + void applyAddnJvmArguments(const QStringList& minecraftArguments); + void applyMinecraftVersionType(const QString& type); + void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); + void applyTraits(const QSet& traits); + void applyTweakers(const QStringList& tweakers); + void applyJarMods(const QList& jarMods); + void applyMods(const QList& jarMods); + void applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext); + void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); + void applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext); + void applyCompatibleJavaMajors(QList& javaMajor); + void applyCompatibleJavaName(QString javaName); + void applyMainJar(LibraryPtr jar); + void applyProblemSeverity(ProblemSeverity severity); + /// clear the profile + void clear(); + + public: /* getters for profile variables */ + QString getMinecraftVersion() const; + QString getMainClass() const; + QString getAppletClass() const; + QString getMinecraftVersionType() const; + MojangAssetIndexInfo::Ptr getMinecraftAssets() const; + QString getMinecraftArguments() const; + const QStringList& getAddnJvmArguments() const; + const QSet& getTraits() const; + const QStringList& getTweakers() const; + const QList& getJarMods() const; + const QList& getLibraries() const; + const QList& getNativeLibraries() const; + const QList& getMavenFiles() const; + const QList& getAgents() const; + const QList& getCompatibleJavaMajors() const; + const QString getCompatibleJavaName() const; + const LibraryPtr getMainJar() const; + void getLibraryFiles(const RuntimeContext& runtimeContext, + QStringList& jars, + QStringList& nativeJars, + const QString& overridePath, + const QString& moddedJarSearchResultPath) const; + bool hasTrait(const QString& trait) const; + ProblemSeverity getProblemSeverity() const override; + const QList getProblems() const override; + + private: + /// the version of Minecraft - jar to use + QString m_minecraftVersion; + + /// Release type - "release" or "snapshot" + QString m_minecraftVersionType; + + /// Assets type - "legacy" or a version ID + MojangAssetIndexInfo::Ptr m_minecraftAssets; + + /** + * arguments that should be used for launching minecraft + * + * ex: "--username ${auth_player_name} --session ${auth_session} + * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" + */ + QString m_minecraftArguments; + + /** + * Additional arguments to pass to the JVM in addition to those the user has configured, + * memory settings, etc. + */ + QStringList m_addnJvmArguments; + + /// A list of all tweaker classes + QStringList m_tweakers; + + /// The main class to load first + QString m_mainClass; + + /// The applet class, for some very old minecraft releases + QString m_appletClass; + + /// the list of libraries + QList m_libraries; + + /// the list of maven files to be placed in the libraries folder, but not acted upon + QList m_mavenFiles; + + /// the list of java agents to add to JVM arguments + QList m_agents; + + /// the main jar + LibraryPtr m_mainJar; + + /// the list of native libraries + QList m_nativeLibraries; + + /// traits, collected from all the version files (version files can only add) + QSet m_traits; + + /// A list of jar mods. version files can add those. + QList m_jarMods; + + /// the list of mods + QList m_mods; + + /// compatible java major versions + QList m_compatibleJavaMajors; + + QString m_compatibleJavaName; + + ProblemSeverity m_problemSeverity = ProblemSeverity::None; +}; diff --git a/archived/projt-launcher/launcher/minecraft/Library.cpp b/archived/projt-launcher/launcher/minecraft/Library.cpp new file mode 100644 index 0000000000..551f572cc8 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Library.cpp @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "Library.h" +#include "MinecraftInstance.h" +#include "net/NetRequest.h" + +#include +#include +#include +#include +#include + +static QString normalizeNeoForgedMavenUrl(const QString& url) +{ + const QUrl parsed(url); + if (!parsed.isValid() || parsed.host().compare("maven.neoforged.net", Qt::CaseInsensitive) != 0) + { + return url; + } + + const auto path = parsed.path(); + if (!path.startsWith("/net/neoforged/") || path.startsWith("/releases/")) + { + return url; + } + + QUrl fixed(parsed); + fixed.setPath("/releases" + path); + return fixed.toString(QUrl::FullyEncoded); +} + +/** + * @brief Collect applicable files for the library. + * + * Depending on whether the library is native or not, it adds paths to the + * appropriate lists for jar files, native libraries for 32-bit, and native + * libraries for 64-bit. + * + * @param runtimeContext The current runtime context. + * @param jar List to store paths for jar files. + * @param native List to store paths for native libraries. + * @param native32 List to store paths for 32-bit native libraries. + * @param native64 List to store paths for 64-bit native libraries. + * @param overridePath Optional path to override the default storage path. + */ +void Library::getApplicableFiles(const RuntimeContext& runtimeContext, + QStringList& jar, + QStringList& native, + QStringList& native32, + QStringList& native64, + const QString& overridePath) const +{ + bool local = isLocal(); + // Lambda function to get the absolute file path + auto actualPath = [this, local, overridePath](QString relPath) + { + relPath = FS::RemoveInvalidPathChars(relPath); + QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); + if (local && !overridePath.isEmpty()) + { + QString fileName = out.fileName(); + return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath(); + } + return out.absoluteFilePath(); + }; + + QString raw_storage = storageSuffix(runtimeContext); + if (isNative()) + { + if (raw_storage.contains("${arch}")) + { + auto nat32Storage = raw_storage; + nat32Storage.replace("${arch}", "32"); + auto nat64Storage = raw_storage; + nat64Storage.replace("${arch}", "64"); + native32 += actualPath(nat32Storage); + native64 += actualPath(nat64Storage); + } + else + { + native += actualPath(raw_storage); + } + } + else + { + jar += actualPath(raw_storage); + } +} + +/** + * @brief Get download requests for the library files. + * + * Depending on whether the library is native or not, and the current runtime context, + * this function prepares download requests for the necessary files. It handles both local + * and remote files, checks for stale cache entries, and adds checksummed downloads. + * + * @param runtimeContext The current runtime context. + * @param cache Pointer to the HTTP meta cache. + * @param failedLocalFiles List to store paths for failed local files. + * @param overridePath Optional path to override the default storage path. + * @return QList List of download requests. + */ +QList Library::getDownloads(const RuntimeContext& runtimeContext, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString& overridePath) const +{ + QList out; + bool stale = isAlwaysStale(); + bool local = isLocal(); + + // Lambda function to check if a local file exists + auto check_local_file = [overridePath, &failedLocalFiles](QString storage) + { + QFileInfo fileinfo(storage); + QString fileName = fileinfo.fileName(); + auto fullPath = FS::PathCombine(overridePath, fileName); + QFileInfo localFileInfo(fullPath); + if (!localFileInfo.exists()) + { + failedLocalFiles.append(localFileInfo.filePath()); + return false; + } + return true; + }; + + // Lambda function to add a download request + auto add_download = [this, local, check_local_file, cache, stale, &out](QString storage, QString url, QString sha1) + { + if (local) + { + return check_local_file(storage); + } + url = normalizeNeoForgedMavenUrl(url); + auto entry = cache->resolveEntry("libraries", storage); + if (stale) + { + entry->setStale(true); + } + if (!entry->isStale()) + return true; + Net::Download::Options options; + if (stale) + { + options |= Net::Download::Option::AcceptLocalFiles; + } + + // Don't add a time limit for the libraries cache entry validity + options |= Net::Download::Option::MakeEternal; + + if (sha1.size()) + { + auto dl = Net::ApiDownload::makeCached(url, entry, options); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + out.append(dl); + } + else + { + out.append(Net::ApiDownload::makeCached(url, entry, options)); + qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + } + return true; + }; + + QString raw_storage = storageSuffix(runtimeContext); + if (m_mojangDownloads) + { + if (isNative()) + { + auto nativeClassifier = getCompatibleNative(runtimeContext); + if (!nativeClassifier.isNull()) + { + if (nativeClassifier.contains("${arch}")) + { + auto nat32Classifier = nativeClassifier; + nat32Classifier.replace("${arch}", "32"); + auto nat64Classifier = nativeClassifier; + nat64Classifier.replace("${arch}", "64"); + auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); + if (nat32info) + { + auto cooked_storage = raw_storage; + cooked_storage.replace("${arch}", "32"); + add_download(cooked_storage, nat32info->url, nat32info->sha1); + } + auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); + if (nat64info) + { + auto cooked_storage = raw_storage; + cooked_storage.replace("${arch}", "64"); + add_download(cooked_storage, nat64info->url, nat64info->sha1); + } + } + else + { + auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier); + if (info) + { + add_download(raw_storage, info->url, info->sha1); + } + } + } + else + { + qDebug() << "Ignoring native library" << m_name.serialize() + << "because it has no classifier for current OS"; + } + } + else + { + if (m_mojangDownloads->artifact) + { + auto artifact = m_mojangDownloads->artifact; + add_download(raw_storage, artifact->url, artifact->sha1); + } + else + { + qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; + } + } + } + else + { + auto raw_dl = [this, raw_storage]() + { + if (!m_absoluteURL.isEmpty()) + { + return m_absoluteURL; + } + + if (m_repositoryURL.isEmpty()) + { + return BuildConfig.LIBRARY_BASE + raw_storage; + } + + if (m_repositoryURL.endsWith('/')) + { + return m_repositoryURL + raw_storage; + } + else + { + return m_repositoryURL + QChar('/') + raw_storage; + } + }(); + if (raw_storage.contains("${arch}")) + { + QString cooked_storage = raw_storage; + QString cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32"), QString()); + cooked_storage = raw_storage; + cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64"), QString()); + } + else + { + add_download(raw_storage, raw_dl, QString()); + } + } + return out; +} + +/** + * @brief Check if the library is active in the given runtime context. + * + * This function evaluates rules to determine if the library should be active, + * considering both general rules and native compatibility. + * + * @param runtimeContext The current runtime context. + * @return bool True if the library is active, false otherwise. + */ +bool Library::isActive(const RuntimeContext& runtimeContext) const +{ + bool result = true; + if (m_rules.empty()) + { + result = true; + } + else + { + Rule::Action ruleResult = Rule::Disallow; + for (auto rule : m_rules) + { + Rule::Action temp = rule.apply(runtimeContext); + if (temp != Rule::Defer) + ruleResult = temp; + } + result = result && (ruleResult == Rule::Allow); + } + if (isNative()) + { + result = result && !getCompatibleNative(runtimeContext).isNull(); + } + return result; +} + +/** + * @brief Check if the library is considered local. + * + * @return bool True if the library is local, false otherwise. + */ +bool Library::isLocal() const +{ + return m_hint == "local"; +} + +/** + * @brief Check if the library is always considered stale. + * + * @return bool True if the library is always stale, false otherwise. + */ +bool Library::isAlwaysStale() const +{ + return m_hint == "always-stale"; +} + +/** + * @brief Get the compatible native classifier for the current runtime context. + * + * This function attempts to match the current runtime context with the appropriate + * native classifier. + * + * @param runtimeContext The current runtime context. + * @return QString The compatible native classifier, or an empty string if none is found. + */ +QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const +{ + // try to match precise classifier "[os]-[arch]" + auto entry = m_nativeClassifiers.constFind(runtimeContext.getClassifier()); + // try to match imprecise classifier on legacy architectures "[os]" + if (entry == m_nativeClassifiers.constEnd() && runtimeContext.isLegacyArch()) + entry = m_nativeClassifiers.constFind(runtimeContext.system); + + if (entry == m_nativeClassifiers.constEnd()) + return QString(); + + return entry.value(); +} + +/** + * @brief Set the storage prefix for the library. + * + * @param prefix The storage prefix to set. + */ +void Library::setStoragePrefix(QString prefix) +{ + m_storagePrefix = prefix; +} + +/** + * @brief Get the default storage prefix for libraries. + * + * @return QString The default storage prefix. + */ +QString Library::defaultStoragePrefix() +{ + return "libraries/"; +} + +/** + * @brief Get the current storage prefix for the library. + * + * @return QString The current storage prefix. + */ +QString Library::storagePrefix() const +{ + if (m_storagePrefix.isEmpty()) + { + return defaultStoragePrefix(); + } + return m_storagePrefix; +} + +/** + * @brief Get the filename for the library in the current runtime context. + * + * This function determines the appropriate filename for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The filename of the library. + */ +QString Library::filename(const RuntimeContext& runtimeContext) const +{ + if (!m_filename.isEmpty()) + { + return m_filename; + } + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.getFileName(); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + QString nativeClassifier = getCompatibleNative(runtimeContext); + if (!nativeClassifier.isNull()) + { + nativeSpec.setClassifier(nativeClassifier); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.getFileName(); +} + +/** + * @brief Get the display name for the library in the current runtime context. + * + * This function returns the display name for the library, defaulting to the filename + * if no display name is set. + * + * @param runtimeContext The current runtime context. + * @return QString The display name of the library. + */ +QString Library::displayName(const RuntimeContext& runtimeContext) const +{ + if (!m_displayname.isEmpty()) + return m_displayname; + return filename(runtimeContext); +} + +/** + * @brief Get the storage suffix for the library in the current runtime context. + * + * This function determines the appropriate storage suffix for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The storage suffix of the library. + */ +QString Library::storageSuffix(const RuntimeContext& runtimeContext) const +{ + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.toPath(m_filename); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + QString nativeClassifier = getCompatibleNative(runtimeContext); + if (!nativeClassifier.isNull()) + { + nativeSpec.setClassifier(nativeClassifier); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.toPath(m_filename); +} diff --git a/archived/projt-launcher/launcher/minecraft/Library.h b/archived/projt-launcher/launcher/minecraft/Library.h new file mode 100644 index 0000000000..2a42df6bff --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Library.h @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GradleSpecifier.h" +#include "MojangDownloadInfo.h" +#include "Rule.h" +#include "RuntimeContext.h" +#include "net/NetRequest.h" + +class Library; +class MinecraftInstance; + +using LibraryPtr = std::shared_ptr; + +class Library +{ + friend class OneSixVersionFormat; + friend class MojangVersionFormat; + friend class LibraryTest; + + public: + Library() + {} + Library(const QString& name) + { + m_name = name; + } + /// Create a copy of the library. + static LibraryPtr limitedCopy(LibraryPtr base) + { + auto newlib = std::make_shared(); + newlib->m_name = base->m_name; + newlib->m_repositoryURL = base->m_repositoryURL; + newlib->m_hint = base->m_hint; + newlib->m_absoluteURL = base->m_absoluteURL; + newlib->m_extractExcludes = base->m_extractExcludes; + newlib->m_nativeClassifiers = base->m_nativeClassifiers; + newlib->m_rules = base->m_rules; + newlib->m_storagePrefix = base->m_storagePrefix; + newlib->m_mojangDownloads = base->m_mojangDownloads; + newlib->m_filename = base->m_filename; + newlib->m_displayname = base->m_displayname; // Full copy + return newlib; + } + + public: /* methods */ + /// Returns the raw name field + const GradleSpecifier& rawName() const + { + return m_name; + } + + void setRawName(const GradleSpecifier& spec) + { + m_name = spec; + } + + void setClassifier(const QString& spec) + { + m_name.setClassifier(spec); + } + + /// returns the full group and artifact prefix + QString artifactPrefix() const + { + return m_name.artifactPrefix(); + } + + /// get the artifact ID + QString artifactId() const + { + return m_name.artifactId(); + } + + /// get the artifact version + QString version() const + { + return m_name.version(); + } + + /// Returns true if the library is native + bool isNative() const + { + return m_nativeClassifiers.size() != 0; + } + + void setStoragePrefix(QString prefix = QString()); + + /// Set the url base for downloads + void setRepositoryURL(const QString& base_url) + { + m_repositoryURL = base_url; + } + + void getApplicableFiles(const RuntimeContext& runtimeContext, + QStringList& jar, + QStringList& native, + QStringList& native32, + QStringList& native64, + const QString& overridePath) const; + + void setAbsoluteUrl(const QString& absolute_url) + { + m_absoluteURL = absolute_url; + } + + void setFilename(const QString& filename) + { + m_filename = filename; + } + + /// Get the file name of the library + QString filename(const RuntimeContext& runtimeContext) const; + + // NOTE: Legacy support for Core Mods display cutoff (approx 1.4.7 era). Used by jar mods only + void setDisplayName(const QString& displayName) + { + m_displayname = displayName; + } + + /// Get the file name of the library + QString displayName(const RuntimeContext& runtimeContext) const; + + void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) + { + m_mojangDownloads = info; + } + + void setHint(const QString& hint) + { + m_hint = hint; + } + + /// Set the load rules + void setRules(QList rules) + { + m_rules = rules; + } + + /// Returns true if the library should be loaded (or extracted, in case of natives) + bool isActive(const RuntimeContext& runtimeContext) const; + + /// Returns true if the library is contained in an instance and false if it is shared + bool isLocal() const; + + /// Returns true if the library is to always be checked for updates + bool isAlwaysStale() const; + + /// Return true if the library requires forge XZ hacks + bool isForge() const; + + // Get a list of downloads for this library + QList getDownloads(const RuntimeContext& runtimeContext, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString& overridePath) const; + + QString getCompatibleNative(const RuntimeContext& runtimeContext) const; + + private: /* methods */ + /// the default storage prefix used by ProjT Launcher + static QString defaultStoragePrefix(); + + /// Get the prefix - root of the storage to be used + QString storagePrefix() const; + + /// Get the relative file path where the library should be saved + QString storageSuffix(const RuntimeContext& runtimeContext) const; + + QString hint() const + { + return m_hint; + } + + protected: /* data */ + /// the basic gradle dependency specifier. + GradleSpecifier m_name; + + /// DEPRECATED URL prefix of the maven repo where the file can be downloaded + QString m_repositoryURL; + + /// DEPRECATED: ProjT Launcher-specific absolute URL. takes precedence over the implicit maven repo URL, if defined + QString m_absoluteURL; + + /// ProjT Launcher extension - filename override + QString m_filename; + + /// DEPRECATED ProjT Launcher extension - display name + QString m_displayname; + + /** + * ProjT Launcher-specific type hint - modifies how the library is treated + */ + QString m_hint; + + /** + * storage - by default the local libraries folder in ProjT Launcher, but could be elsewhere + * ProjT Launcher specific, because of FTB. + */ + QString m_storagePrefix; + + /// true if the library had an extract/excludes section (even empty) + bool m_hasExcludes = false; + + /// a list of files that shouldn't be extracted from the library + QStringList m_extractExcludes; + + /// native suffixes per OS + QMap m_nativeClassifiers; + + /// true if the library had a rules section (even empty) + bool applyRules = false; + + /// rules associated with the library + QList m_rules; + + /// MOJANG: container with Mojang style download info + MojangLibraryDownloadInfo::Ptr m_mojangDownloads; +}; diff --git a/archived/projt-launcher/launcher/minecraft/Logging.cpp b/archived/projt-launcher/launcher/minecraft/Logging.cpp new file mode 100644 index 0000000000..63862cce77 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Logging.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "minecraft/Logging.h" +#include + +Q_LOGGING_CATEGORY(instanceProfileC, "launcher.instance.profile") +Q_LOGGING_CATEGORY(instanceProfileResolveC, "launcher.instance.profile.resolve") +Q_LOGGING_CATEGORY(authCredentials, "launcher.auth.credentials") diff --git a/archived/projt-launcher/launcher/minecraft/Logging.h b/archived/projt-launcher/launcher/minecraft/Logging.h new file mode 100644 index 0000000000..02aa363346 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Logging.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include + +Q_DECLARE_LOGGING_CATEGORY(instanceProfileC) +Q_DECLARE_LOGGING_CATEGORY(instanceProfileResolveC) +Q_DECLARE_LOGGING_CATEGORY(authCredentials) diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstance.cpp b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.cpp new file mode 100644 index 0000000000..29e44954ad --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.cpp @@ -0,0 +1,1430 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "MinecraftInstance.h" +#include "LaunchMode.h" +#include "Application.h" +#include "BuildConfig.h" +#include "Commandline.h" +#include "Json.h" +#include "MinecraftInstanceLaunchMenu.h" +#include "QObjectPtr.h" +#include "minecraft/launch/AutoInstallJava.hpp" +#include "minecraft/launch/CreateGameFolders.hpp" +#include "minecraft/launch/ExtractNatives.hpp" +#include "minecraft/launch/PrintInstanceInfo.hpp" +#include "minecraft/update/AssetUpdateTask.h" +#include "minecraft/update/FMLLibrariesTask.h" +#include "minecraft/update/LibrariesTask.h" +#include "settings/Setting.h" +#include "settings/SettingsObject.h" + +#include "FileSystem.h" +#include "MMCTime.h" +#include "java/core/RuntimeVersion.hpp" + +#include "launch/LaunchPipeline.hpp" +#include "launch/TaskBridgeStage.hpp" +#include "launch/steps/RuntimeProbeStep.hpp" +#include "launch/steps/ServerJoinResolveStep.hpp" +#include "launch/steps/LaunchCommandStep.hpp" +#include "launch/steps/QuitAfterGameStep.hpp" +#include "launch/steps/LogMessageStep.hpp" + +#include "minecraft/launch/ClaimAccount.hpp" +#include "minecraft/launch/LauncherPartLaunch.hpp" +#include "minecraft/launch/ModMinecraftJar.hpp" +#include "minecraft/launch/ReconstructAssets.hpp" +#include "minecraft/launch/ScanModFolders.hpp" +#include "minecraft/launch/VerifyJavaInstall.hpp" + +#include "java/services/RuntimeEnvironment.hpp" + +#include "icons/IconList.hpp" + +#include "mod/ModFolderModel.hpp" +#include "mod/ResourcePackFolderModel.hpp" +#include "mod/ShaderPackFolderModel.hpp" +#include "mod/TexturePackFolderModel.hpp" + +#include "WorldList.h" + +#include "AssetsUtils.h" +#include "MinecraftLoadAndCheck.h" +#include "PackProfile.h" +#include "minecraft/update/FoldersTask.h" + +#include "tools/BaseProfiler.h" + +#include +#include +#include +#include + +#ifdef Q_OS_LINUX +#include "MangoHud.h" +#endif + +#ifdef WITH_QTDBUS +#include +#endif + +#define IBUS "@im=ibus" + +[[maybe_unused]] static bool switcherooSetupGPU(QProcessEnvironment& env) +{ +#ifdef WITH_QTDBUS + if (!QDBusConnection::systemBus().isConnected()) + return false; + + QDBusInterface switcheroo("net.hadess.SwitcherooControl", + "/net/hadess/SwitcherooControl", + "org.freedesktop.DBus.Properties", + QDBusConnection::systemBus()); + + if (!switcheroo.isValid()) + return false; + + QDBusReply reply = + switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs")); + if (!reply.isValid()) + return false; + + QDBusArgument arg = qvariant_cast(reply.value().variant()); + QList gpus; + arg >> gpus; + + for (const auto& gpu : gpus) + { + QString name = qvariant_cast(gpu[QStringLiteral("Name")]); + bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); + if (!defaultGpu) + { + QStringList envList = qvariant_cast(gpu[QStringLiteral("Environment")]); + for (int i = 0; i + 1 < envList.size(); i += 2) + { + env.insert(envList[i], envList[i + 1]); + } + return true; + } + } +#endif + return false; +} + +// all of this because keeping things compatible with deprecated old settings +// if either of the settings {a, b} is true, this also resolves to true +class OrSetting : public Setting +{ + Q_OBJECT + public: + OrSetting(QString id, std::shared_ptr a, std::shared_ptr b) + : Setting({ id }, false), + m_a(a), + m_b(b) + {} + virtual QVariant get() const + { + bool a = m_a->get().toBool(); + bool b = m_b->get().toBool(); + return a || b; + } + virtual void reset() + {} + virtual void set(QVariant value) + {} + + private: + std::shared_ptr m_a; + std::shared_ptr m_b; +}; + +MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, + SettingsObjectPtr settings, + const QString& rootDir) + : BaseInstance(globalSettings, settings, rootDir) +{ + m_components.reset(new PackProfile(this)); +} + +void MinecraftInstance::saveNow() +{ + m_components->saveNow(); +} + +void MinecraftInstance::loadSpecificSettings() +{ + if (isSpecificSettingsLoaded()) + return; + + // Java Settings + auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); + auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); + m_settings->registerSetting("AutomaticJava", false); + + if (auto global_settings = globalSettings()) + { + m_settings->registerOverride(global_settings->getSetting("JavaPath"), locationOverride); + m_settings->registerOverride(global_settings->getSetting("JvmArgs"), argsOverride); + m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), locationOverride); + + // special! + m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), locationOverride); + + // Window Size + auto windowSetting = m_settings->registerSetting("OverrideWindow", false); + m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting); + m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting); + m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting); + + // Memory + auto memorySetting = m_settings->registerSetting("OverrideMemory", false); + m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); + m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); + m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); + + // Native library workarounds + auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); + m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("CustomOpenALPath"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); + + // Performance related options + auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); + m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride); + m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride); + m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride); + m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride); + + // Miscellaneous + auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); + m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); + m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + + // Legacy-related options + auto legacySettings = m_settings->registerSetting("OverrideLegacySettings", false); + m_settings->registerOverride(global_settings->getSetting("OnlineFixes"), legacySettings); + + auto envSetting = m_settings->registerSetting("OverrideEnv", false); + m_settings->registerOverride(global_settings->getSetting("Env"), envSetting); + + m_settings->set("InstanceType", "OneSix"); + } + + // Join server on launch, this does not have a global override + m_settings->registerSetting("JoinServerOnLaunch", false); + m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + m_settings->registerSetting("JoinWorldOnLaunch", ""); + + // Use account for instance, this does not have a global override + m_settings->registerSetting("UseAccountForInstance", false); + m_settings->registerSetting("InstanceAccountId", ""); + + m_settings->registerSetting("ExportName", ""); + m_settings->registerSetting("ExportVersion", "1.0.0"); + m_settings->registerSetting("ExportSummary", ""); + m_settings->registerSetting("ExportAuthor", ""); + m_settings->registerSetting("ExportOptionalFiles", true); + m_settings->registerSetting("ExportRecommendedRAM"); + + auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); + auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); + + connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + + // Join server on launch, this does not have a global override + m_settings->registerSetting("OverrideModDownloadLoaders", false); + m_settings->registerSetting("ModDownloadLoaders", "[]"); + + qDebug() << "Instance-type specific settings were loaded!"; + + setSpecificSettingsLoaded(true); + + updateRuntimeContext(); +} + +void MinecraftInstance::updateRuntimeContext() +{ + m_runtimeContext.updateFromInstanceSettings(m_settings); + m_components->invalidateLaunchProfile(); +} + +QString MinecraftInstance::typeName() const +{ + return "Minecraft"; +} + +std::shared_ptr MinecraftInstance::getPackProfile() const +{ + return m_components; +} + +QSet MinecraftInstance::traits() const +{ + auto components = getPackProfile(); + if (!components) + { + return { "version-incomplete" }; + } + auto profile = components->getProfile(); + if (!profile) + { + return { "version-incomplete" }; + } + return profile->getTraits(); +} + +QString MinecraftInstance::gameRoot() const +{ + QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); + + if (dotMCDir.exists() && !mcDir.exists()) + return dotMCDir.filePath(); + else + return mcDir.filePath(); +} + +QString MinecraftInstance::binRoot() const +{ + return FS::PathCombine(gameRoot(), "bin"); +} + +QString MinecraftInstance::getNativePath() const +{ + QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); + return natives_dir.absolutePath(); +} + +QString MinecraftInstance::getLocalLibraryPath() const +{ + QDir libraries_dir(FS::PathCombine(instanceRoot(), "libraries/")); + return libraries_dir.absolutePath(); +} + +bool MinecraftInstance::supportsDemo() const +{ + Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") }; + // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History + // Note: This check may not work correctly for non-release versions due to version string formatting. Demo support + // is based on release versions. + return instance_ver >= Version("1.3.1"); +} + +QString MinecraftInstance::jarModsDir() const +{ + QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); + return jarmods_dir.absolutePath(); +} + +QString MinecraftInstance::modsRoot() const +{ + return FS::PathCombine(gameRoot(), "mods"); +} + +QString MinecraftInstance::modsCacheLocation() const +{ + return FS::PathCombine(instanceRoot(), "mods.cache"); +} + +QString MinecraftInstance::coreModsDir() const +{ + return FS::PathCombine(gameRoot(), "coremods"); +} + +QString MinecraftInstance::nilModsDir() const +{ + return FS::PathCombine(gameRoot(), "nilmods"); +} + +QString MinecraftInstance::dataPacksDir() +{ + QString relativePath = settings()->get("GlobalDataPacksPath").toString(); + + if (relativePath.isEmpty()) + relativePath = "datapacks"; + + return QDir(gameRoot()).filePath(relativePath); +} + +QString MinecraftInstance::resourcePacksDir() const +{ + return FS::PathCombine(gameRoot(), "resourcepacks"); +} + +QString MinecraftInstance::texturePacksDir() const +{ + return FS::PathCombine(gameRoot(), "texturepacks"); +} + +QString MinecraftInstance::shaderPacksDir() const +{ + return FS::PathCombine(gameRoot(), "shaderpacks"); +} + +QString MinecraftInstance::instanceConfigFolder() const +{ + return FS::PathCombine(gameRoot(), "config"); +} + +QString MinecraftInstance::libDir() const +{ + return FS::PathCombine(gameRoot(), "lib"); +} + +QString MinecraftInstance::worldDir() const +{ + return FS::PathCombine(gameRoot(), "saves"); +} + +QString MinecraftInstance::resourcesDir() const +{ + return FS::PathCombine(gameRoot(), "resources"); +} + +QDir MinecraftInstance::librariesPath() const +{ + return QDir::current().absoluteFilePath("libraries"); +} + +QDir MinecraftInstance::jarmodsPath() const +{ + return QDir(jarModsDir()); +} + +QDir MinecraftInstance::versionsPath() const +{ + return QDir::current().absoluteFilePath("versions"); +} + +QStringList MinecraftInstance::getClassPath() +{ + QStringList jars, nativeJars; + auto profile = m_components->getProfile(); + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + return jars; +} + +QString MinecraftInstance::getMainClass() const +{ + auto profile = m_components->getProfile(); + return profile->getMainClass(); +} + +QStringList MinecraftInstance::getNativeJars() +{ + QStringList jars, nativeJars; + auto profile = m_components->getProfile(); + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + return nativeJars; +} + +QStringList MinecraftInstance::extraArguments() +{ + auto list = Commandline::splitArgs(settings()->get("JvmArgs").toString()); + auto version = getPackProfile(); + if (!version) + return list; + auto jarMods = getJarMods(); + if (!jarMods.isEmpty()) + { + list.append({ "-Dfml.ignoreInvalidMinecraftCertificates=true", "-Dfml.ignorePatchDiscrepancies=true" }); + } + auto addn = m_components->getProfile()->getAddnJvmArguments(); + if (!addn.isEmpty()) + { + list.append(addn); + } + auto agents = m_components->getProfile()->getAgents(); + for (auto agent : agents) + { + QStringList jar, temp1, temp2, temp3; + agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); + list.append("-javaagent:" + jar[0] + (agent->argument().isEmpty() ? "" : "=" + agent->argument())); + } + + { + QString openALPath; + QString glfwPath; + + if (settings()->get("UseNativeOpenAL").toBool()) + { + openALPath = APPLICATION->m_detectedOpenALPath; + auto customPath = settings()->get("CustomOpenALPath").toString(); + if (!customPath.isEmpty()) + openALPath = customPath; + } + if (settings()->get("UseNativeGLFW").toBool()) + { + glfwPath = APPLICATION->m_detectedGLFWPath; + auto customPath = settings()->get("CustomGLFWPath").toString(); + if (!customPath.isEmpty()) + glfwPath = customPath; + } + + QFileInfo openALInfo(openALPath); + QFileInfo glfwInfo(glfwPath); + + if (!openALPath.isEmpty() && openALInfo.exists()) + list.append("-Dorg.lwjgl.openal.libname=" + openALInfo.absoluteFilePath()); + if (!glfwPath.isEmpty() && glfwInfo.exists()) + list.append("-Dorg.lwjgl.glfw.libname=" + glfwInfo.absoluteFilePath()); + } + + return list; +} + +QStringList MinecraftInstance::javaArguments() +{ + QStringList args; + + // custom args go first. we want to override them if we have our own here. + args.append(extraArguments()); + + // OSX dock icon and name +#ifdef Q_OS_MAC + args << "-Xdock:icon=icon.png"; + args << QString("-Xdock:name=\"%1\"").arg(windowTitle()); +#endif + auto traits_ = traits(); + // HACK: fix issues on macOS with 1.13 snapshots + // NOTE: Oracle Java option. if there are alternate jvm implementations, this would be the place to customize this + // for them +#ifdef Q_OS_MAC + if (traits_.contains("FirstThreadOnMacOS")) + { + args << QString("-XstartOnFirstThread"); + } +#endif + + // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 +#ifdef Q_OS_WIN32 + args << QString("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" + "minecraft.exe.heapdump"); +#endif + + int min = settings()->get("MinMemAlloc").toInt(); + int max = settings()->get("MaxMemAlloc").toInt(); + if (min < max) + { + args << QString("-Xms%1m").arg(min); + args << QString("-Xmx%1m").arg(max); + } + else + { + args << QString("-Xms%1m").arg(max); + args << QString("-Xmx%1m").arg(min); + } + + // No PermGen in newer java. + projt::java::RuntimeVersion javaVersion = getRuntimeVersion(); + if (javaVersion.needsPermGen()) + { + auto permgen = settings()->get("PermGen").toInt(); + if (permgen != 64) + { + args << QString("-XX:PermSize=%1m").arg(permgen); + } + } + + args << "-Duser.language=en"; + + if (javaVersion.supportsModules() && shouldApplyOnlineFixes()) + // allow reflective access to java.net - required by the skin fix + args << "--add-opens" + << "java.base/java.net=ALL-UNNAMED"; + + return args; +} + +QString MinecraftInstance::getLauncher() +{ + // use legacy launcher if the traits are set + if (isLegacy()) + return "legacy"; + + return "standard"; +} + +bool MinecraftInstance::shouldApplyOnlineFixes() +{ + return traits().contains("legacyServices") && settings()->get("OnlineFixes").toBool(); +} + +QMap MinecraftInstance::getVariables() +{ + QMap out; + out.insert("INST_NAME", name()); + out.insert("INST_ID", id()); + out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); + out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); + out.insert("INST_JAVA", QDir::toNativeSeparators(QDir(settings()->get("JavaPath").toString()).absolutePath())); + out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); + out.insert("NO_COLOR", "1"); +#ifdef Q_OS_MACOS + // get library for Steam overlay support + QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES"); + if (!steamDyldInsertLibraries.isEmpty()) + { + out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries); + } +#endif + return out; +} + +QProcessEnvironment MinecraftInstance::createEnvironment() +{ + // prepare the process environment + QProcessEnvironment env = projt::java::buildCleanEnvironment(); + + // export some infos + auto variables = getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + env.insert(it.key(), it.value()); + } + // custom env + + auto insertEnv = [&env](QString value) + { + auto envMap = Json::toMap(value); + if (envMap.isEmpty()) + return; + + for (auto iter = envMap.begin(); iter != envMap.end(); iter++) + env.insert(iter.key(), iter.value().toString()); + }; + + bool overrideEnv = settings()->get("OverrideEnv").toBool(); + + if (!overrideEnv) + insertEnv(APPLICATION->settings()->get("Env").toString()); + else + insertEnv(settings()->get("Env").toString()); + return env; +} + +QProcessEnvironment MinecraftInstance::createLaunchEnvironment() +{ + // prepare the process environment + QProcessEnvironment env = createEnvironment(); + +#ifdef Q_OS_LINUX + if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud) + { + QStringList preloadList; + if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) + preloadList = value.split(QLatin1String(":")); + + auto mangoHudLibString = MangoHud::getLibraryString(); + if (!mangoHudLibString.isEmpty()) + { + QFileInfo mangoHudLib(mangoHudLibString); + QString libPath = mangoHudLib.absolutePath(); + auto appendLib = [libPath, &preloadList](QString fileName) + { + if (QFileInfo(FS::PathCombine(libPath, fileName)).exists()) + preloadList << FS::PathCombine(libPath, fileName); + }; + + // dlsym variant is only needed for OpenGL and not included in the vulkan layer + appendLib("libMangoHud_dlsym.so"); + appendLib("libMangoHud_opengl.so"); + appendLib("libMangoHud_shim.so"); + preloadList << mangoHudLibString; + } + + env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); + env.insert("MANGOHUD", "1"); + } + + if (settings()->get("UseDiscreteGpu").toBool()) + { + if (!switcherooSetupGPU(env)) + { + // Open Source Drivers + env.insert("DRI_PRIME", "1"); + // Proprietary Nvidia Drivers + env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); + env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); + env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + } + } + + if (settings()->get("UseZink").toBool()) + { + // taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink) + env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa"); + env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink"); + env.insert("GALLIUM_DRIVER", "zink"); + } +#endif + return env; +} + +static QString replaceTokensIn(QString text, QMap with) +{ + // Replace tokens in the format ${key} with values from the map. + QString result; + static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); + QStringList list; + QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); + int lastCapturedEnd = 0; + while (i.hasNext()) + { + QRegularExpressionMatch match = i.next(); + result.append(text.mid(lastCapturedEnd, match.capturedStart())); + QString key = match.captured(1); + auto iter = with.find(key); + if (iter != with.end()) + { + result.append(*iter); + } + lastCapturedEnd = match.capturedEnd(); + } + result.append(text.mid(lastCapturedEnd)); + return result; +} + +QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const +{ + auto profile = m_components->getProfile(); + QString args_pattern = profile->getMinecraftArguments(); + for (auto tweaker : profile->getTweakers()) + { + args_pattern += " --tweakClass " + tweaker; + } + + if (targetToJoin) + { + if (!targetToJoin->address.isEmpty()) + { + if (profile->hasTrait("feature:is_quick_play_multiplayer")) + { + args_pattern += + " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); + } + else + { + args_pattern += " --server " + targetToJoin->address; + args_pattern += " --port " + QString::number(targetToJoin->port); + } + } + else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) + { + args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; + } + } + + QMap token_mapping; + // yggdrasil! + if (session) + { + // token_mapping["auth_username"] = session->username; + token_mapping["auth_session"] = session->session; + token_mapping["auth_access_token"] = session->access_token; + token_mapping["auth_player_name"] = session->player_name; + token_mapping["auth_uuid"] = session->uuid; + token_mapping["user_properties"] = session->serializeUserProperties(); + token_mapping["user_type"] = session->user_type; + if (session->launchMode == LaunchMode::Demo) + { + args_pattern += " --demo"; + } + } + + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = profile->getMinecraftVersion(); + token_mapping["version_type"] = profile->getMinecraftVersionType(); + + QString absRootDir = QDir(gameRoot()).absolutePath(); + token_mapping["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + auto assets = profile->getMinecraftAssets(); + token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); + + // 1.7.3+ assets tokens + token_mapping["assets_root"] = absAssetsDir; + token_mapping["assets_index_name"] = assets->id; + + QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); + for (int i = 0; i < parts.length(); i++) + { + parts[i] = replaceTokensIn(parts[i], token_mapping); + } + return parts; +} + +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) +{ + QString launchScript; + + if (!m_components) + return QString(); + auto profile = m_components->getProfile(); + if (!profile) + return QString(); + + auto mainClass = getMainClass(); + if (!mainClass.isEmpty()) + { + launchScript += "mainClass " + mainClass + "\n"; + } + auto appletClass = profile->getAppletClass(); + if (!appletClass.isEmpty()) + { + launchScript += "appletClass " + appletClass + "\n"; + } + + if (targetToJoin) + { + if (!targetToJoin->address.isEmpty()) + { + launchScript += "serverAddress " + targetToJoin->address + "\n"; + launchScript += "serverPort " + QString::number(targetToJoin->port) + "\n"; + } + else if (!targetToJoin->world.isEmpty()) + { + launchScript += "worldName " + targetToJoin->world + "\n"; + } + } + + // generic minecraft params + for (auto param : processMinecraftArgs(session, nullptr /* When using a launch script, the server parameters are + handled by it*/ + )) + { + launchScript += "param " + param + "\n"; + } + + // window size, title and state, legacy + { + QString windowParams; + if (settings()->get("LaunchMaximized").toBool()) + { + // Note: Fullscreen/maximization support is limited. UI enhancements for fullscreen mode are needed. + if (!isLegacy()) + { + auto screen = QGuiApplication::primaryScreen(); + auto screenGeometry = screen->availableSize(); + + // small hack to get the widow decorations + for (auto w : QApplication::topLevelWidgets()) + { + auto mainWindow = qobject_cast(w); + if (mainWindow) + { + auto m = mainWindow->windowHandle()->frameMargins(); + screenGeometry = screenGeometry.shrunkBy(m); + break; + } + } + + windowParams = QString("%1x%2").arg(screenGeometry.width()).arg(screenGeometry.height()); + } + else + { + windowParams = "maximized"; + } + } + else + { + windowParams = QString("%1x%2") + .arg(settings()->get("MinecraftWinWidth").toInt()) + .arg(settings()->get("MinecraftWinHeight").toInt()); + } + launchScript += "windowTitle " + windowTitle() + "\n"; + launchScript += "windowParams " + windowParams + "\n"; + } + + // launcher info + { + launchScript += "launcherBrand " + BuildConfig.LAUNCHER_NAME + "\n"; + launchScript += "launcherVersion " + BuildConfig.printableVersionString() + "\n"; + } + + // instance info + { + launchScript += "instanceName " + name() + "\n"; + launchScript += "instanceIconKey " + name() + "\n"; + launchScript += "instanceIconPath icon.png\n"; // we already save a copy here + } + + // legacy auth + if (session) + { + launchScript += "userName " + session->player_name + "\n"; + launchScript += "sessionId " + session->session + "\n"; + } + + for (auto trait : profile->getTraits()) + { + launchScript += "traits " + trait + "\n"; + } + + if (shouldApplyOnlineFixes()) + launchScript += "onlineFixes true\n"; + + launchScript += "launcher " + getLauncher() + "\n"; + + // qDebug() << "Generated launch script:" << launchScript; + return launchScript; +} + +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) +{ + QStringList out; + out << "Main Class:" + << " " + getMainClass() << ""; + out << "Native path:" + << " " + getNativePath() << ""; + + auto profile = m_components->getProfile(); + + // traits + auto alltraits = traits(); + if (alltraits.size()) + { + out << "Traits:"; + for (auto trait : alltraits) + { + out << "traits " + trait; + } + out << ""; + } + + // native libraries + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) + { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << ""; + } + + // libraries and class path. + { + out << "Libraries:"; + QStringList jars, nativeJars; + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + auto printLibFile = [&out](const QString& path) + { + QFileInfo info(path); + if (info.exists()) + { + out << " " + path; + } + else + { + out << " " + path + " (missing)"; + } + }; + for (auto file : jars) + { + printLibFile(file); + } + out << ""; + out << "Native libraries:"; + for (auto file : nativeJars) + { + printLibFile(file); + } + out << ""; + } + + // mods and core mods + auto printModList = [&out](const QString& label, ModFolderModel& model) + { + if (model.size()) + { + out << QString("%1:").arg(label); + auto modList = model.allMods(); + std::sort(modList.begin(), + modList.end(), + [](auto a, auto b) + { + auto aName = a->fileinfo().completeBaseName(); + auto bName = b->fileinfo().completeBaseName(); + return aName.localeAwareCompare(bName) < 0; + }); + for (auto mod : modList) + { + if (mod->type() == ResourceType::FOLDER) + { + out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; + continue; + } + + if (mod->enabled()) + { + out << u8" [✔] " + mod->fileinfo().completeBaseName(); + } + else + { + out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; + } + } + out << ""; + } + }; + + printModList("Mods", *(loaderModList().get())); + printModList("Core Mods", *(coreModList().get())); + + // jar mods + auto& jarMods = profile->getJarMods(); + if (jarMods.size()) + { + out << "Jar Mods:"; + for (auto& jarmod : jarMods) + { + auto displayname = jarmod->displayName(runtimeContext()); + auto realname = jarmod->filename(runtimeContext()); + if (displayname != realname) + { + out << " " + displayname + " (" + realname + ")"; + } + else + { + out << " " + realname; + } + } + out << ""; + } + + // minecraft arguments + auto params = processMinecraftArgs(nullptr, targetToJoin); + out << "Params:"; + out << " " + params.join(' '); + out << ""; + + // window size + QString windowParams; + if (settings->get("LaunchMaximized").toBool()) + { + out << "Window size: max (if available)"; + } + else + { + auto width = settings->get("MinecraftWinWidth").toInt(); + auto height = settings->get("MinecraftWinHeight").toInt(); + out << "Window size: " + QString::number(width) + " x " + QString::number(height); + } + out << ""; + out << "Launcher: " + getLauncher(); + out << ""; + return out; +} + +QMap MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session) +{ + if (!session) + { + return QMap(); + } + auto& sessionRef = *session.get(); + QMap filter; + auto addToFilter = [&filter](QString key, QString value) + { + if (key.trimmed().size()) + { + filter[key] = value; + } + }; + if (sessionRef.session != "-") + { + addToFilter(sessionRef.session, tr("")); + } + if (sessionRef.access_token != "0") + { + addToFilter(sessionRef.access_token, tr("")); + } + addToFilter(sessionRef.uuid, tr("")); + + return filter; +} + +QStringList MinecraftInstance::getLogFileSearchPaths() +{ + return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; +} + +QString MinecraftInstance::getStatusbarDescription() +{ + QStringList traits; + if (hasVersionBroken()) + { + traits.append(tr("broken")); + } + + QString mcVersion = m_components->getComponentVersion("net.minecraft"); + if (mcVersion.isEmpty()) + { + // Load component info if needed - use Online mode to fetch metadata if not cached + m_components->reload(Net::Mode::Online); + mcVersion = m_components->getComponentVersion("net.minecraft"); + } + + QString description; + description.append(tr("Minecraft %1").arg(mcVersion)); + if (m_settings->get("ShowGameTime").toBool()) + { + if (lastTimePlayed() > 0 && lastLaunch() > 0) + { + QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); + description.append( + tr(", last played on %1 for %2") + .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) + .arg(Time::prettifyDuration(lastTimePlayed(), + APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); + } + + if (totalTimePlayed() > 0) + { + description.append( + tr(", total played for %1") + .arg(Time::prettifyDuration(totalTimePlayed(), + APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); + } + } + if (hasCrashed()) + { + description.append(tr(", has crashed.")); + } + return description; +} + +QList MinecraftInstance::createUpdateTask() +{ + return { + // create folders + makeShared(this), + // libraries download + makeShared(this), + // FML libraries download and copy into the instance + makeShared(this), + // assets update + makeShared(this), + }; +} + +shared_qobject_ptr MinecraftInstance::createLaunchPipeline( + AuthSessionPtr session, + MinecraftTarget::Ptr targetToJoin) +{ + updateRuntimeContext(); + // Using static_pointer_cast since 'this' is guaranteed to be a MinecraftInstance + auto process = + projt::launch::LaunchPipeline::create(std::dynamic_pointer_cast(shared_from_this())); + auto pptr = process.get(); + + APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); + + // print a header + { + process->appendStage( + makeShared(pptr, + "Minecraft folder is:\n" + gameRoot() + "\n\n", + MessageLevel::Launcher)); + } + + // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) + { + process->appendStage(makeShared(pptr)); + } + + if (!targetToJoin && settings()->get("JoinServerOnLaunch").toBool()) + { + QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString(); + if (!fullAddress.isEmpty()) + { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false))); + } + else + { + QString world = settings()->get("JoinWorldOnLaunch").toString(); + if (!world.isEmpty()) + { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(world, true))); + } + } + } + + if (targetToJoin && targetToJoin->port == 25565) + { + // Resolve server address to join on launch + auto step = makeShared(pptr); + step->setLookupAddress(targetToJoin->address); + step->setOutputTarget(targetToJoin); + process->appendStage(step); + } + + // load meta + { + auto mode = session->launchMode != LaunchMode::Offline ? Net::Mode::Online : Net::Mode::Offline; + process->appendStage( + makeShared(pptr, makeShared(this, mode))); + } + + // check java + { + process->appendStage(makeShared(pptr)); + process->appendStage(makeShared(pptr)); + } + + // run pre-launch command if that's needed + if (getPreLaunchCommand().size()) + { + auto step = makeShared( + pptr, + projt::launch::steps::LaunchCommandStep::Hook::PreLaunch, + getPreLaunchCommand()); + step->setWorkingDirectory(gameRoot()); + process->appendStage(step); + } + + // if we aren't in offline mode + if (session->launchMode != LaunchMode::Offline) + { + process->appendStage(makeShared(pptr, session)); + for (auto t : createUpdateTask()) + { + process->appendStage(makeShared(pptr, t)); + } + } + + // if there are any jar mods + { + process->appendStage(makeShared(pptr)); + } + + // Scan mods folders for mods + { + process->appendStage(makeShared(pptr)); + } + + // print some instance info here... + { + process->appendStage(makeShared(pptr, session, targetToJoin)); + } + + // extract native jars if needed + { + process->appendStage(makeShared(pptr)); + } + + // reconstruct assets if needed + { + process->appendStage(makeShared(pptr)); + } + + // verify that minimum Java requirements are met + { + process->appendStage(makeShared(pptr)); + } + + { + // actually launch the game + auto step = makeShared(pptr); + step->setWorkingDirectory(gameRoot()); + step->setAuthSession(session); + step->setTargetToJoin(targetToJoin); + process->appendStage(step); + } + + // run post-exit command if that's needed + if (getPostExitCommand().size()) + { + auto step = + makeShared(pptr, + projt::launch::steps::LaunchCommandStep::Hook::PostExit, + getPostExitCommand()); + step->setWorkingDirectory(gameRoot()); + process->appendStage(step); + } + if (session) + { + process->setCensorFilter(createCensorFilterFromSession(session)); + } + if (m_settings->get("QuitAfterGameStop").toBool()) + { + process->appendStage(makeShared(pptr)); + } + m_launchProcess = process; + emit launchPipelineChanged(m_launchProcess); + return m_launchProcess; +} + +projt::java::RuntimeVersion MinecraftInstance::getRuntimeVersion() +{ + return projt::java::RuntimeVersion(settings()->get("JavaVersion").toString()); +} + +std::shared_ptr MinecraftInstance::loaderModList() +{ + if (!m_loader_mod_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed, true)); + } + return m_loader_mod_list; +} + +std::shared_ptr MinecraftInstance::coreModList() +{ + if (!m_core_mod_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed, true)); + } + return m_core_mod_list; +} + +std::shared_ptr MinecraftInstance::nilModList() +{ + if (!m_nil_mod_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); + } + return m_nil_mod_list; +} + +std::shared_ptr MinecraftInstance::resourcePackList() +{ + if (!m_resource_pack_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this, is_indexed, true)); + } + return m_resource_pack_list; +} + +std::shared_ptr MinecraftInstance::texturePackList() +{ + if (!m_texture_pack_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this, is_indexed, true)); + } + return m_texture_pack_list; +} + +std::shared_ptr MinecraftInstance::shaderPackList() +{ + if (!m_shader_pack_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this, is_indexed, true)); + } + return m_shader_pack_list; +} + +std::shared_ptr MinecraftInstance::dataPackList() +{ + if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) + { + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); + } + return m_data_pack_list; +} + +QList> MinecraftInstance::resourceLists() +{ + return { loaderModList(), coreModList(), nilModList(), resourcePackList(), + texturePackList(), shaderPackList(), dataPackList() }; +} + +std::shared_ptr MinecraftInstance::worldList() +{ + if (!m_world_list) + { + m_world_list.reset(new WorldList(worldDir(), this)); + } + return m_world_list; +} + +QList MinecraftInstance::getJarMods() const +{ + auto profile = m_components->getProfile(); + QList mods; + for (auto jarmod : profile->getJarMods()) + { + QStringList jar, temp1, temp2, temp3; + jarmod->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); + // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); + mods.push_back(new Mod(QFileInfo(jar[0]))); + } + return mods; +} + +#include "MinecraftInstance.moc" diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstance.h b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.h new file mode 100644 index 0000000000..49242bee5b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.h @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once +#include +#include +#include +#include +#include "BaseInstance.h" +#include "minecraft/launch/MinecraftTarget.hpp" +#include "minecraft/mod/Mod.hpp" + +class ModFolderModel; +class ResourceFolderModel; +class ResourcePackFolderModel; +class ShaderPackFolderModel; +class TexturePackFolderModel; +class WorldList; +namespace projt::launch +{ + class LaunchPipeline; +} +class PackProfile; + +class MinecraftInstance : public BaseInstance +{ + Q_OBJECT + public: + MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); + virtual ~MinecraftInstance() = default; + virtual void saveNow() override; + + void loadSpecificSettings() override; + + /// @deprecated Legacy method - consider moving to PackProfile + QString typeName() const override; + /// @deprecated Legacy method - traits should come from PackProfile components + QSet traits() const override; + + bool canEdit() const override + { + return true; + } + + bool canExport() const override + { + return true; + } + + ////// Directories and files ////// + + ////// Directories and files ////// + QString jarModsDir() const; + QString resourcePacksDir() const; + QString texturePacksDir() const; + QString shaderPacksDir() const; + QString modsRoot() const override; + QString coreModsDir() const; + QString nilModsDir() const; + QString dataPacksDir(); + QString modsCacheLocation() const; + QString libDir() const; + QString worldDir() const; + QString resourcesDir() const; + QDir jarmodsPath() const; + QDir librariesPath() const; + QDir versionsPath() const; + QString instanceConfigFolder() const override; + + // Path to the instance's minecraft directory. + QString gameRoot() const override; + + // Path to the instance's minecraft bin directory. + QString binRoot() const; + + // where to put the natives during/before launch + QString getNativePath() const; + + // where the instance-local libraries should be + QString getLocalLibraryPath() const; + + /** Returns whether the instance, with its version, has support for demo mode. */ + bool supportsDemo() const; + + void updateRuntimeContext() override; + + ////// Profile management ////// + std::shared_ptr getPackProfile() const; + + ////// Mod Lists ////// + std::shared_ptr loaderModList(); + std::shared_ptr coreModList(); + std::shared_ptr nilModList(); + std::shared_ptr resourcePackList(); + std::shared_ptr texturePackList(); + std::shared_ptr shaderPackList(); + std::shared_ptr dataPackList(); + QList> resourceLists(); + std::shared_ptr worldList(); + + ////// Launch stuff ////// + QList createUpdateTask() override; + shared_qobject_ptr createLaunchPipeline(AuthSessionPtr account, + MinecraftTarget::Ptr targetToJoin) override; + QStringList extraArguments(); + QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; + QList getJarMods() const; + QString createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin); + /// get arguments passed to java + QStringList javaArguments(); + QString getLauncher(); + bool shouldApplyOnlineFixes(); + + /// get variables for launch command variable substitution/environment + QMap getVariables() override; + + /// create an environment for launching processes + QProcessEnvironment createEnvironment() override; + QProcessEnvironment createLaunchEnvironment() override; + + QStringList getLogFileSearchPaths() override; + + QString getStatusbarDescription() override; + + /// @deprecated These methods expose internal launch profile data. Use LaunchProfile directly. + virtual QStringList getClassPath(); + /// @deprecated Use LaunchProfile::getNativeLibraries() instead + virtual QStringList getNativeJars(); + /// @deprecated Use LaunchProfile::getMainClass() instead + virtual QString getMainClass() const; + + /// @deprecated Argument processing should be in launch steps, not instance + virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) const; + + virtual projt::java::RuntimeVersion getRuntimeVersion(); + + signals: + void profilerChanged(); + + protected: + QMap createCensorFilterFromSession(AuthSessionPtr session); + + protected: // data + std::shared_ptr m_components; + mutable std::shared_ptr m_loader_mod_list; + mutable std::shared_ptr m_core_mod_list; + mutable std::shared_ptr m_nil_mod_list; + mutable std::shared_ptr m_resource_pack_list; + mutable std::shared_ptr m_shader_pack_list; + mutable std::shared_ptr m_texture_pack_list; + mutable std::shared_ptr m_data_pack_list; + mutable std::shared_ptr m_world_list; +}; + +using MinecraftInstancePtr = std::shared_ptr; diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.cpp b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.cpp new file mode 100644 index 0000000000..aad5512b44 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "MinecraftInstanceLaunchMenu.h" + +#include +#include +#include + +#include +#include "tools/BaseProfiler.h" + +#include "MinecraftInstance.h" + +void MinecraftInstanceLaunchMenu::populate(MinecraftInstance* instance, QMenu* menu) +{ + QAction* normalLaunch = menu->addAction(MinecraftInstance::tr("&Launch")); + normalLaunch->setShortcut(QKeySequence::Open); + QAction* normalLaunchOffline = menu->addAction(MinecraftInstance::tr("Launch &Offline")); + normalLaunchOffline->setShortcut(QKeySequence(MinecraftInstance::tr("Ctrl+Shift+O"))); + QAction* normalLaunchDemo = menu->addAction(MinecraftInstance::tr("Launch &Demo")); + normalLaunchDemo->setShortcut(QKeySequence(MinecraftInstance::tr("Ctrl+Alt+O"))); + + normalLaunchDemo->setEnabled(instance->supportsDemo()); + + QObject::connect(normalLaunch, + &QAction::triggered, + [instance] { APPLICATION->launch(instance->shared_from_this()); }); + QObject::connect(normalLaunchOffline, + &QAction::triggered, + [instance] { APPLICATION->launch(instance->shared_from_this(), LaunchMode::Offline); }); + QObject::connect(normalLaunchDemo, + &QAction::triggered, + [instance] { APPLICATION->launch(instance->shared_from_this(), LaunchMode::Demo); }); + + QString profilersTitle = MinecraftInstance::tr("Profilers"); + menu->addSeparator()->setText(profilersTitle); + + auto profilers = new QActionGroup(menu); + profilers->setExclusive(true); + QObject::connect(profilers, + &QActionGroup::triggered, + [instance](QAction* action) + { + instance->settings()->set("Profiler", action->data()); + emit instance->profilerChanged(); + }); + + QAction* noProfilerAction = menu->addAction(MinecraftInstance::tr("&No Profiler")); + noProfilerAction->setData(""); + noProfilerAction->setCheckable(true); + noProfilerAction->setChecked(true); + profilers->addAction(noProfilerAction); + + for (auto profiler = APPLICATION->profilers().begin(); profiler != APPLICATION->profilers().end(); profiler++) + { + QAction* profilerAction = menu->addAction(profiler.value()->name()); + profilers->addAction(profilerAction); + profilerAction->setData(profiler.key()); + profilerAction->setCheckable(true); + profilerAction->setChecked(instance->settings()->get("Profiler").toString() == profiler.key()); + + QString error; + profilerAction->setEnabled(profiler.value()->check(&error)); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.h b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.h new file mode 100644 index 0000000000..309c890cb6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +class MinecraftInstance; + +/** + * Helper class to populate launch menu for Minecraft instances. + * This separates UI code from the core MinecraftInstance class. + */ +class MinecraftInstanceLaunchMenu +{ + public: + static void populate(MinecraftInstance* instance, QMenu* menu); +}; diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.cpp b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.cpp new file mode 100644 index 0000000000..5ea700e6b7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "MinecraftLoadAndCheck.h" +#include "MinecraftInstance.h" +#include "PackProfile.h" + +MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode) + : m_inst(inst), + m_netmode(netmode) +{} + +void MinecraftLoadAndCheck::executeTask() +{ + // add offline metadata load task + auto components = m_inst->getPackProfile(); + if (auto result = components->reload(m_netmode); !result) + { + emitFailed(result.error); + return; + } + m_task = components->getCurrentTask(); + + if (!m_task) + { + emitSucceeded(); + return; + } + connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::emitSucceeded); + connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::emitFailed); + connect(m_task.get(), &Task::aborted, this, &MinecraftLoadAndCheck::emitAborted); + connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::setProgress); + connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propagateStepProgress); + connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); + connect(m_task.get(), &Task::details, this, &MinecraftLoadAndCheck::setDetails); +} + +bool MinecraftLoadAndCheck::canAbort() const +{ + if (m_task) + { + return m_task->canAbort(); + } + return true; +} + +bool MinecraftLoadAndCheck::abort() +{ + if (m_task && m_task->canAbort()) + { + return m_task->abort(); + } + return Task::abort(); +} diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.h b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.h new file mode 100644 index 0000000000..f0f13f343d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include "net/Mode.h" +#include "tasks/Task.h" + +class MinecraftInstance; + +class MinecraftLoadAndCheck : public Task +{ + Q_OBJECT + public: + explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode); + virtual ~MinecraftLoadAndCheck() = default; + void executeTask() override; + + bool canAbort() const override; + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst = nullptr; + Task::Ptr m_task; + Net::Mode m_netmode; +}; diff --git a/archived/projt-launcher/launcher/minecraft/MojangDownloadInfo.h b/archived/projt-launcher/launcher/minecraft/MojangDownloadInfo.h new file mode 100644 index 0000000000..4efe23328a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MojangDownloadInfo.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include +#include + +struct MojangDownloadInfo +{ + // types + using Ptr = std::shared_ptr; + + // data + /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested! + QString path; + /// absolute URL of this file + QString url; + /// sha-1 checksum of the file + QString sha1; + /// size of the file in bytes + int size; +}; + +struct MojangLibraryDownloadInfo +{ + MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact_) : artifact(artifact_) + {} + MojangLibraryDownloadInfo() + {} + + // types + using Ptr = std::shared_ptr; + + // methods + MojangDownloadInfo* getDownloadInfo(QString classifier) + { + if (classifier.isNull()) + { + return artifact.get(); + } + + return classifiers[classifier].get(); + } + + // data + MojangDownloadInfo::Ptr artifact; + QMap classifiers; +}; + +struct MojangAssetIndexInfo : public MojangDownloadInfo +{ + // types + using Ptr = std::shared_ptr; + + // methods + MojangAssetIndexInfo() + {} + + MojangAssetIndexInfo(QString id_) + { + this->id = id_; + // HACK: ignore assets from other version files than Minecraft + // workaround for stupid assets issue caused by amazon: + // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ + if (id_ == "legacy") + { + url = + "https://piston-meta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; + } + // HACK + else + { + url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id_ + ".json"; + } + known = false; + } + + // data + int totalSize; + QString id; + bool known = true; +}; diff --git a/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.cpp b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.cpp new file mode 100644 index 0000000000..21b8fda1b7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.cpp @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "MojangVersionFormat.h" +#include "MojangDownloadInfo.h" +#include "OneSixVersionFormat.h" + +#include "Json.h" +using namespace Json; +#include +#include "ParseUtils.h" + +static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18; + +static MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj); +static MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj); +static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj); +static QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr assetidxinfo); +static QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo); +static QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info); + +namespace Bits +{ + static void readString(const QJsonObject& root, const QString& key, QString& variable) + { + if (root.contains(key)) + { + variable = requireString(root.value(key)); + } + } + + static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject& obj) + { + // optional, not used + readString(obj, "path", out->path); + // required! + out->sha1 = requireString(obj, "sha1"); + out->url = requireString(obj, "url"); + out->size = requireInteger(obj, "size"); + } + + static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject& obj) + { + out->totalSize = requireInteger(obj, "totalSize"); + out->id = requireString(obj, "id"); + // out->known = true; + } +} // namespace Bits + +MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj) +{ + auto out = std::make_shared(); + Bits::readDownloadInfo(out, obj); + return out; +} + +MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj) +{ + auto out = std::make_shared(); + Bits::readDownloadInfo(out, obj); + Bits::readAssetIndex(out, obj); + return out; +} + +QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info) +{ + QJsonObject out; + if (!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + return out; +} + +MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj) +{ + auto out = std::make_shared(); + auto dlObj = requireObject(libObj.value("downloads")); + if (dlObj.contains("artifact")) + { + out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact")); + } + if (dlObj.contains("classifiers")) + { + auto classifiersObj = requireObject(dlObj, "classifiers"); + for (auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->classifiers[classifier] = downloadInfoFromJson(classifierObj); + } + } + return out; +} + +QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) +{ + QJsonObject out; + if (libinfo->artifact) + { + out.insert("artifact", downloadInfoToJson(libinfo->artifact)); + } + if (!libinfo->classifiers.isEmpty()) + { + QJsonObject classifiersOut; + for (auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) + { + classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("classifiers", classifiersOut); + } + return out; +} + +QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info) +{ + QJsonObject out; + if (!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + out.insert("totalSize", info->totalSize); + out.insert("id", info->id); + return out; +} + +void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFile* out) +{ + Bits::readString(in, "id", out->minecraftVersion); + Bits::readString(in, "mainClass", out->mainClass); + Bits::readString(in, "minecraftArguments", out->minecraftArguments); + Bits::readString(in, "type", out->type); + + Bits::readString(in, "assets", out->assets); + if (in.contains("assetIndex")) + { + out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex")); + } + else if (!out->assets.isNull()) + { + out->mojangAssetIndex = std::make_shared(out->assets); + } + + out->releaseTime = timeFromS3Time(in.value("releaseTime").toString("")); + out->updateTime = timeFromS3Time(in.value("time").toString("")); + + if (in.contains("minimumLauncherVersion")) + { + out->minimumLauncherVersion = requireInteger(in.value("minimumLauncherVersion")); + if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) + { + out->addProblem(ProblemSeverity::Warning, + QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than " + "supported by %3 (%2). It might not work properly!") + .arg(out->minimumLauncherVersion) + .arg(CURRENT_MINIMUM_LAUNCHER_VERSION) + .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + } + } + + if (in.contains("compatibleJavaMajors")) + { + for (auto compatible : requireArray(in.value("compatibleJavaMajors"))) + { + out->compatibleJavaMajors.append(requireInteger(compatible)); + } + } + if (in.contains("compatibleJavaName")) + { + out->compatibleJavaName = requireString(in.value("compatibleJavaName")); + } + + if (in.contains("downloads")) + { + auto downloadsObj = requireObject(in, "downloads"); + for (auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj); + } + } +} + +VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument& doc, const QString& filename) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + readVersionProperties(root, out.get()); + + out->name = "Minecraft"; + out->uid = "net.minecraft"; + out->version = out->minecraftVersion; + // out->filename = filename; + + if (root.contains("libraries")) + { + for (auto libVal : requireArray(root.value("libraries"))) + { + auto libObj = requireObject(libVal); + + auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); + out->libraries.append(lib); + } + } + return out; +} + +void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out) +{ + writeString(out, "id", in->minecraftVersion); + writeString(out, "mainClass", in->mainClass); + writeString(out, "minecraftArguments", in->minecraftArguments); + writeString(out, "type", in->type); + if (!in->releaseTime.isNull()) + { + writeString(out, "releaseTime", timeToS3Time(in->releaseTime)); + } + if (!in->updateTime.isNull()) + { + writeString(out, "time", timeToS3Time(in->updateTime)); + } + if (in->minimumLauncherVersion != -1) + { + out.insert("minimumLauncherVersion", in->minimumLauncherVersion); + } + writeString(out, "assets", in->assets); + if (in->mojangAssetIndex && in->mojangAssetIndex->known) + { + out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); + } + if (!in->mojangDownloads.isEmpty()) + { + QJsonObject downloadsOut; + for (auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) + { + downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("downloads", downloadsOut); + } + if (!in->compatibleJavaMajors.isEmpty()) + { + QJsonArray compatibleJavaMajorsOut; + for (auto compatibleJavaMajor : in->compatibleJavaMajors) + { + compatibleJavaMajorsOut.append(compatibleJavaMajor); + } + out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); + } + if (!in->compatibleJavaName.isEmpty()) + { + writeString(out, "compatibleJavaName", in->compatibleJavaName); + } +} + +QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr& patch) +{ + QJsonObject root; + writeVersionProperties(patch.get(), root); + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value : patch->libraries) + { + array.append(MojangVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); + } + auto rawName = libObj.value("name").toString(); + out->m_name = rawName; + if (!out->m_name.valid()) + { + problems.addProblem(ProblemSeverity::Error, + QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); + } + + Bits::readString(libObj, "url", out->m_repositoryURL); + if (libObj.contains("extract")) + { + out->m_hasExcludes = true; + auto extractObj = requireObject(libObj.value("extract")); + for (auto excludeVal : requireArray(extractObj.value("exclude"))) + { + out->m_extractExcludes.append(requireString(excludeVal)); + } + } + if (libObj.contains("natives")) + { + QJsonObject nativesObj = requireObject(libObj.value("natives")); + for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) + { + if (!it.value().isString()) + { + qWarning() << filename << "contains an invalid native (skipping)"; + continue; + } + // Skip unknown platforms + QString platform = it.key(); + if (platform != "linux" && platform != "windows" && platform != "osx" && platform != "macos" + && platform != "freebsd" && platform != "openbsd" && platform != "netbsd") + { + qWarning() << filename << "contains unknown platform" << platform << "(skipping)"; + continue; + } + out->m_nativeClassifiers[it.key()] = it.value().toString(); + } + } + if (libObj.contains("rules")) + { + out->applyRules = true; + + QJsonArray rulesArray = requireArray(libObj.value("rules")); + for (auto rule : rulesArray) + { + out->m_rules.append(Rule::fromJson(requireObject(rule))); + } + } + if (libObj.contains("downloads")) + { + out->m_mojangDownloads = libDownloadInfoFromJson(libObj); + } + return out; +} + +QJsonObject MojangVersionFormat::libraryToJson(Library* library) +{ + QJsonObject libRoot; + libRoot.insert("name", library->m_name.serialize()); + if (!library->m_repositoryURL.isEmpty()) + { + libRoot.insert("url", library->m_repositoryURL); + } + if (library->isNative()) + { + QJsonObject nativeList; + auto iter = library->m_nativeClassifiers.begin(); + while (iter != library->m_nativeClassifiers.end()) + { + nativeList.insert(iter.key(), iter.value()); + iter++; + } + libRoot.insert("natives", nativeList); + if (!library->m_extractExcludes.isEmpty()) + { + QJsonArray excludes; + QJsonObject extract; + for (auto exclude : library->m_extractExcludes) + { + excludes.append(exclude); + } + extract.insert("exclude", excludes); + libRoot.insert("extract", extract); + } + } + if (!library->m_rules.isEmpty()) + { + QJsonArray allRules; + for (auto& rule : library->m_rules) + { + QJsonObject ruleObj = rule.toJson(); + allRules.append(ruleObj); + } + libRoot.insert("rules", allRules); + } + if (library->m_mojangDownloads) + { + auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads); + libRoot.insert("downloads", downloadsObj); + } + return libRoot; +} diff --git a/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.h b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.h new file mode 100644 index 0000000000..26eb64692a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include + +class MojangVersionFormat +{ + friend class OneSixVersionFormat; + + protected: + // does not include libraries + static void readVersionProperties(const QJsonObject& in, VersionFile* out); + // does not include libraries + static void writeVersionProperties(const VersionFile* in, QJsonObject& out); + + public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename); + static QJsonDocument versionFileToJson(const VersionFilePtr& patch); + + // libraries + static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject libraryToJson(Library* library); +}; diff --git a/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.cpp b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.cpp new file mode 100644 index 0000000000..6aba9fe5df --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.cpp @@ -0,0 +1,513 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "OneSixVersionFormat.h" +#include +#include +#include +#include "java/core/RuntimePackage.hpp" +#include "minecraft/Agent.h" +#include "minecraft/ParseUtils.h" + +#include + +using namespace Json; + +static void readString(const QJsonObject& root, const QString& key, QString& variable) +{ + if (root.contains(key)) + { + variable = requireString(root.value(key)); + } +} + +LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); + readString(libObj, "MMC-hint", out->m_hint); + readString(libObj, "MMC-absulute_url", out->m_absoluteURL); + readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); + readString(libObj, "MMC-filename", out->m_filename); + readString(libObj, "MMC-displayname", out->m_displayname); + return out; +} + +QJsonObject OneSixVersionFormat::libraryToJson(Library* library) +{ + QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); + if (!library->m_absoluteURL.isEmpty()) + libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); + if (!library->m_hint.isEmpty()) + libRoot.insert("MMC-hint", library->m_hint); + if (!library->m_filename.isEmpty()) + libRoot.insert("MMC-filename", library->m_filename); + if (!library->m_displayname.isEmpty()) + libRoot.insert("MMC-displayname", library->m_displayname); + return libRoot; +} + +VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc, + const QString& filename, + const bool requireOrder) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + projt::meta::SchemaVersion formatVersion = projt::meta::detectSchemaVersion(root, false); + switch (formatVersion) + { + case projt::meta::SchemaVersion::V1: break; + case projt::meta::SchemaVersion::Unknown: + throw JSONValidationError(filename + " does not contain a recognizable version of the metadata format."); + } + + if (requireOrder) + { + if (root.contains("order")) + { + out->order = requireInteger(root.value("order")); + } + else + { + // Order is required but missing - this is an error condition + throw JSONValidationError(filename + " requires an order field but doesn't contain one."); + } + } + + out->name = root.value("name").toString(); + + if (root.contains("uid")) + { + out->uid = root.value("uid").toString(); + } + else + { + out->uid = root.value("fileId").toString(); + } + + static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( + QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; + if (!s_validUidRegex.match(out->uid).hasMatch()) + { + qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; + out->addProblem( + ProblemSeverity::Error, + QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); + } + + out->version = root.value("version").toString(); + + MojangVersionFormat::readVersionProperties(root, out.get()); + + if (root.contains("+tweakers")) + { + for (auto tweakerVal : requireArray(root.value("+tweakers"))) + { + out->addTweakers.append(requireString(tweakerVal)); + } + } + + if (root.contains("+traits")) + { + for (auto tweakerVal : requireArray(root.value("+traits"))) + { + out->traits.insert(requireString(tweakerVal)); + } + } + + if (root.contains("+jvmArgs")) + { + for (auto arg : requireArray(root.value("+jvmArgs"))) + { + out->addnJvmArguments.append(requireString(arg)); + } + } + + if (root.contains("jarMods")) + { + for (auto libVal : requireArray(root.value("jarMods"))) + { + QJsonObject libObj = requireObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); + // and add to jar mods + out->jarMods.append(lib); + } + } + else if (root.contains("+jarMods")) // DEPRECATED: old style '+jarMods' are only here for backwards compatibility + { + for (auto libVal : requireArray(root.value("+jarMods"))) + { + QJsonObject libObj = requireObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); + // and add to jar mods + out->jarMods.append(lib); + } + } + + if (root.contains("mods")) + { + for (auto libVal : requireArray(root.value("mods"))) + { + QJsonObject libObj = requireObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); + // and add to jar mods + out->mods.append(lib); + } + } + + auto readLibs = [&root, &out, &filename](const char* which, QList& outList) + { + for (auto libVal : requireArray(root.value(which))) + { + QJsonObject libObj = requireObject(libVal); + // parse the library + auto lib = libraryFromJson(*out, libObj, filename); + outList.append(lib); + } + }; + bool hasPlusLibs = root.contains("+libraries"); + bool hasLibs = root.contains("libraries"); + if (hasPlusLibs && hasLibs) + { + out->addProblem( + ProblemSeverity::Warning, + QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); + readLibs("libraries", out->libraries); + readLibs("+libraries", out->libraries); + } + else if (hasLibs) + { + readLibs("libraries", out->libraries); + } + else if (hasPlusLibs) + { + readLibs("+libraries", out->libraries); + } + + if (root.contains("mavenFiles")) + { + readLibs("mavenFiles", out->mavenFiles); + } + + if (root.contains("+agents")) + { + for (auto agentVal : requireArray(root.value("+agents"))) + { + QJsonObject agentObj = requireObject(agentVal); + auto lib = libraryFromJson(*out, agentObj, filename); + + QString arg = ""; + readString(agentObj, "argument", arg); + + AgentPtr agent(new Agent(lib, arg)); + out->agents.append(agent); + } + } + + // if we have mainJar, just use it + if (root.contains("mainJar")) + { + QJsonObject libObj = requireObject(root, "mainJar"); + out->mainJar = libraryFromJson(*out, libObj, filename); + } + // else reconstruct it from downloads and id ... if that's available + else if (!out->minecraftVersion.isEmpty()) + { + auto lib = std::make_shared(); + lib->setRawName(GradleSpecifier(QString("com.mojang:minecraft:%1:client").arg(out->minecraftVersion))); + // we have a reliable client download, use it. + if (out->mojangDownloads.contains("client")) + { + auto LibDLInfo = std::make_shared(); + LibDLInfo->artifact = out->mojangDownloads["client"]; + lib->setMojangDownloadInfo(LibDLInfo); + } + // we got nothing... + else + { + out->addProblem(ProblemSeverity::Error, + QObject::tr("URL for the main jar could not be determined - Mojang removed the server that " + "we used as fallback.")); + } + out->mainJar = lib; + } + + if (root.contains("requires")) + { + out->m_requires = projt::meta::parseDependencies(root, "requires"); + } + QString dependsOnMinecraftVersion = root.value("mcVersion").toString(); + if (!dependsOnMinecraftVersion.isEmpty()) + { + projt::meta::ComponentDependency mcReq; + mcReq.uid = "net.minecraft"; + mcReq.equalsVersion = dependsOnMinecraftVersion; + if (out->m_requires.count(mcReq) == 0) + { + out->m_requires.insert(mcReq); + } + } + if (root.contains("conflicts")) + { + out->conflicts = projt::meta::parseDependencies(root, "conflicts"); + } + if (root.contains("volatile")) + { + out->m_volatile = requireBoolean(root, "volatile"); + } + + if (root.contains("runtimes")) + { + out->runtimes = {}; + for (auto runtime : ensureArray(root, "runtimes")) + { + out->runtimes.append(projt::java::parseRuntimePackage(ensureObject(runtime))); + } + } + + /* removed features that shouldn't be used */ + if (root.contains("tweakers")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'")); + } + if (root.contains("-libraries")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-libraries'")); + } + if (root.contains("-tweakers")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-tweakers'")); + } + if (root.contains("-minecraftArguments")) + { + out->addProblem(ProblemSeverity::Error, + QObject::tr("Version file contains unsupported element '-minecraftArguments'")); + } + if (root.contains("+minecraftArguments")) + { + out->addProblem(ProblemSeverity::Error, + QObject::tr("Version file contains unsupported element '+minecraftArguments'")); + } + return out; +} + +QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch) +{ + QJsonObject root; + writeString(root, "name", patch->name); + + writeString(root, "uid", patch->uid); + + writeString(root, "version", patch->version); + + projt::meta::writeSchemaVersion(root, projt::meta::SchemaVersion::V1); + + MojangVersionFormat::writeVersionProperties(patch.get(), root); + + if (patch->mainJar) + { + root.insert("mainJar", libraryToJson(patch->mainJar.get())); + } + writeString(root, "appletClass", patch->appletClass); + writeStringList(root, "+tweakers", patch->addTweakers); + writeStringList(root, "+traits", patch->traits.values()); + writeStringList(root, "+jvmArgs", patch->addnJvmArguments); + if (!patch->agents.isEmpty()) + { + QJsonArray array; + for (auto value : patch->agents) + { + QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); + if (!value->argument().isEmpty()) + agentOut.insert("argument", value->argument()); + + array.append(agentOut); + } + root.insert("+agents", array); + } + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value : patch->libraries) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + if (!patch->mavenFiles.isEmpty()) + { + QJsonArray array; + for (auto value : patch->mavenFiles) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("mavenFiles", array); + } + if (!patch->jarMods.isEmpty()) + { + QJsonArray array; + for (auto value : patch->jarMods) + { + array.append(OneSixVersionFormat::jarModtoJson(value.get())); + } + root.insert("jarMods", array); + } + if (!patch->mods.isEmpty()) + { + QJsonArray array; + for (auto value : patch->jarMods) + { + array.append(OneSixVersionFormat::modtoJson(value.get())); + } + root.insert("mods", array); + } + if (!patch->m_requires.empty()) + { + projt::meta::writeDependencies(root, patch->m_requires, "requires"); + } + if (!patch->conflicts.empty()) + { + projt::meta::writeDependencies(root, patch->conflicts, "conflicts"); + } + if (patch->m_volatile) + { + root.insert("volatile", true); + } + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename, + const QString& originalName) +{ + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + "contains a jarmod that doesn't have a 'name' field"); + } + + // just make up something unique on the spot for the library name. + QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); + + // filename override is the old name + out->setFilename(libObj.value("name").toString()); + + // it needs to be local, it is stored in the instance jarmods folder + out->setHint("local"); + + // read the original name if present - some versions did not set it + // it is the original jar mod filename before it got renamed at the point of addition + auto displayName = libObj.value("originalName").toString(); + if (displayName.isEmpty()) + { + auto fixed = originalName; + fixed.remove(" (jar mod)"); + out->setDisplayName(fixed); + } + else + { + out->setDisplayName(displayName); + } + return out; +} + +LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + return libraryFromJson(problems, libObj, filename); +} + +QJsonObject OneSixVersionFormat::jarModtoJson(Library* jarmod) +{ + return libraryToJson(jarmod); +} + +LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + return libraryFromJson(problems, libObj, filename); +} + +QJsonObject OneSixVersionFormat::modtoJson(Library* jarmod) +{ + return libraryToJson(jarmod); +} diff --git a/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.h b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.h new file mode 100644 index 0000000000..669997ff0a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include +#include + +class OneSixVersionFormat +{ + public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename, bool requireOrder); + static QJsonDocument versionFileToJson(const VersionFilePtr& patch); + + // libraries + static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject libraryToJson(Library* library); + + // DEPRECATED: old 'plus' jar mods generated by the application + static LibraryPtr plusJarModFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename, + const QString& originalName); + + // new jar mods derived from libraries + static LibraryPtr jarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject jarModtoJson(Library* jarmod); + + // mods, also derived from libraries + static LibraryPtr modFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject modtoJson(Library* jarmod); +}; diff --git a/archived/projt-launcher/launcher/minecraft/PackProfile.cpp b/archived/projt-launcher/launcher/minecraft/PackProfile.cpp new file mode 100644 index 0000000000..0fd908e8a6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/PackProfile.cpp @@ -0,0 +1,1311 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022-2023 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "Exception.h" +#include "FileSystem.h" +#include "Json.h" +#include "meta/Index.hpp" +#include "meta/JsonFormat.hpp" +#include "minecraft/Component.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/ProfileUtils.h" + +#include "ComponentUpdateTask.h" +#include "PackProfile.h" +#include "PackProfile_p.h" +#include "modplatform/ModIndex.h" + +#include "minecraft/Logging.h" + +#include "ui/dialogs/CustomMessageBox.h" + +PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() +{ + d.reset(new PackProfileData); + d->m_instance = instance; + d->m_saveTimer.setSingleShot(true); + d->m_saveTimer.setInterval(5000); + d->interactionDisabled = instance->isRunning(); + connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction); + connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal); +} + +PackProfile::~PackProfile() +{ + saveNow(); +} + +// BEGIN: component file format + +static const int currentComponentsFileVersion = 1; + +static QJsonObject componentToJsonV1(ComponentPtr component) +{ + QJsonObject obj; + // critical + obj.insert("uid", component->m_uid); + if (!component->m_version.isEmpty()) + { + obj.insert("version", component->m_version); + } + if (component->m_dependencyOnly) + { + obj.insert("dependencyOnly", true); + } + if (component->m_important) + { + obj.insert("important", true); + } + if (component->m_disabled) + { + obj.insert("disabled", true); + } + + // cached + if (!component->m_cachedVersion.isEmpty()) + { + obj.insert("cachedVersion", component->m_cachedVersion); + } + if (!component->m_cachedName.isEmpty()) + { + obj.insert("cachedName", component->m_cachedName); + } + projt::meta::writeDependencies(obj, component->m_cachedRequires, "cachedRequires"); + projt::meta::writeDependencies(obj, component->m_cachedConflicts, "cachedConflicts"); + if (component->m_cachedVolatile) + { + obj.insert("cachedVolatile", true); + } + return obj; +} + +static ComponentPtr componentFromJsonV1(PackProfile* parent, + const QString& componentJsonPattern, + const QJsonObject& obj) +{ + // critical + auto uid = Json::requireString(obj.value("uid")); + auto filePath = componentJsonPattern.arg(uid); + auto component = makeShared(parent, uid); + component->m_version = Json::ensureString(obj.value("version")); + component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); + component->m_important = Json::ensureBoolean(obj.value("important"), false); + + // Cached values - use safe parsing with fallbacks for resilience + // Invalid or missing values are silently ignored to allow loading of + // partially corrupted profiles + try + { + component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion")); + } + catch (...) + { + component->m_cachedVersion = QString(); + } + + try + { + component->m_cachedName = Json::ensureString(obj.value("cachedName")); + } + catch (...) + { + component->m_cachedName = QString(); + } + + try + { + component->m_cachedRequires = projt::meta::parseDependencies(obj, "cachedRequires"); + } + catch (...) + { + component->m_cachedRequires = {}; + } + + try + { + component->m_cachedConflicts = projt::meta::parseDependencies(obj, "cachedConflicts"); + } + catch (...) + { + component->m_cachedConflicts = {}; + } + + component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false); + bool disabled = Json::ensureBoolean(obj.value("disabled"), false); + component->setEnabled(!disabled); + return component; +} + +// Save the given component container data to a file +static bool savePackProfile(const QString& filename, const ComponentContainer& container) +{ + QJsonObject obj; + obj.insert("formatVersion", currentComponentsFileVersion); + QJsonArray orderArray; + for (auto component : container) + { + orderArray.append(componentToJsonV1(component)); + } + obj.insert("components", orderArray); + QSaveFile outFile(filename); + if (!outFile.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << "Couldn't open" << outFile.fileName() + << "for writing:" << outFile.errorString(); + return false; + } + auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); + if (outFile.write(data) != data.size()) + { + qCCritical(instanceProfileC) << "Couldn't write all the data into" << outFile.fileName() + << "because:" << outFile.errorString(); + return false; + } + if (!outFile.commit()) + { + qCCritical(instanceProfileC) << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString(); + } + return true; +} + +// Read the given file into component containers +static PackProfile::Result loadPackProfile(PackProfile* parent, + const QString& filename, + const QString& componentJsonPattern, + ComponentContainer& container) +{ + QFile componentsFile(filename); + if (!componentsFile.exists()) + { + auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename); + qCWarning(instanceProfileC) << message; + return PackProfile::Result::Error(message); + } + if (!componentsFile.open(QFile::ReadOnly)) + { + auto message = QObject::tr("Couldn't open %1 for reading: %2") + .arg(componentsFile.fileName(), componentsFile.errorString()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "Ignoring overridden order"; + return PackProfile::Result::Error(message); + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "Ignoring overridden order"; + return PackProfile::Result::Error(message); + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireInteger(obj.value("formatVersion")); + if (version != currentComponentsFileVersion) + { + throw JSONValidationError( + QObject::tr("Invalid component file version, expected %1").arg(currentComponentsFileVersion)); + } + auto orderArray = Json::requireArray(obj.value("components")); + for (auto item : orderArray) + { + auto comp_obj = Json::requireObject(item, "Component must be an object."); + container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); + } + } + catch ([[maybe_unused]] const JSONValidationError& err) + { + auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "error:" << err.what(); + container.clear(); + return PackProfile::Result::Error(message); + } + return PackProfile::Result::Success(); +} + +// END: component file format + +// BEGIN: save/load logic + +void PackProfile::saveNow() +{ + if (saveIsScheduled()) + { + d->m_saveTimer.stop(); + save_internal(); + } +} + +bool PackProfile::saveIsScheduled() const +{ + return d->dirty; +} + +void PackProfile::buildingFromScratch() +{ + d->loaded = true; + d->dirty = true; +} + +void PackProfile::scheduleSave() +{ + if (!d->loaded) + { + qDebug() << d->m_instance->name() << "|" + << "Component list should never save if it didn't successfully load"; + return; + } + if (!d->dirty) + { + d->dirty = true; + qDebug() << d->m_instance->name() << "|" + << "Component list save is scheduled"; + } + d->m_saveTimer.start(); +} + +RuntimeContext PackProfile::runtimeContext() +{ + return d->m_instance->runtimeContext(); +} + +QString PackProfile::componentsFilePath() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); +} + +QString PackProfile::patchesPattern() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); +} + +QString PackProfile::patchFilePathForUid(const QString& uid) const +{ + return patchesPattern().arg(uid); +} + +void PackProfile::save_internal() +{ + qDebug() << d->m_instance->name() << "|" + << "Component list save performed now"; + auto filename = componentsFilePath(); + savePackProfile(filename, d->components); + d->dirty = false; +} + +PackProfile::Result PackProfile::load() +{ + auto filename = componentsFilePath(); + + // load the new component list and swap it with the current one... + ComponentContainer newComponents; + if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) + { + qCritical() << d->m_instance->name() << "|" + << "Failed to load the component config"; + return result; + } + + // Optimization: check if there are any changes before resetting the model + bool changed = false; + if (d->components.size() != newComponents.size()) + { + changed = true; + } + else + { + for (int i = 0; i < d->components.size(); ++i) + { + const auto& oldC = d->components[i]; + const auto& newC = newComponents[i]; + if (oldC->getID() != newC->getID() || oldC->getVersion() != newC->getVersion() + || oldC->m_important != newC->m_important || oldC->m_disabled != newC->m_disabled + || oldC->m_dependencyOnly != newC->m_dependencyOnly) + { + changed = true; + break; + } + } + } + + if (!changed) + { + d->loaded = true; + return Result::Success(false); + } + + // NOTE: actually use fine-grained updates, not this... + beginResetModel(); + // disconnect all the old components + for (auto component : d->components) + { + disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + } + d->components.clear(); + d->componentIndex.clear(); + for (auto component : newComponents) + { + if (d->componentIndex.contains(component->m_uid)) + { + qWarning() << d->m_instance->name() << "|" + << "Ignoring duplicate component entry" << component->m_uid; + continue; + } + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + d->components.append(component); + d->componentIndex[component->m_uid] = component; + } + endResetModel(); + d->loaded = true; + return Result::Success(); +} + +PackProfile::Result PackProfile::reload(Net::Mode netmode) +{ + // Do not reload when the update/resolve task is running. It is in control. + if (d->m_updateTask) + { + return Result::Success(); + } + + // flush any scheduled saves to not lose state + saveNow(); + + auto result = load(); + if (!result) + { + return result; + } + + if (result.changed) + { + invalidateLaunchProfile(); + } + + resolve(netmode); + return Result::Success(); +} + +Task::Ptr PackProfile::getCurrentTask() +{ + return d->m_updateTask; +} + +void PackProfile::resolve(Net::Mode netmode) +{ + // Use Mode::Launch to ensure version details are downloaded, not just version lists + auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Launch, netmode, this); + d->m_updateTask.reset(updateTask); + connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); + connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); + connect(updateTask, &ComponentUpdateTask::aborted, this, [this] { updateFailed(tr("Aborted")); }); + d->m_updateTask->start(); +} + +void PackProfile::updateSucceeded() +{ + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Component list update/resolve task succeeded"; + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +void PackProfile::updateFailed(const QString& error) +{ + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Component list update/resolve task failed " + << "Reason:" << error; + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +// END: save/load + +void PackProfile::appendComponent(ComponentPtr component) +{ + insertComponent(d->components.size(), component); +} + +void PackProfile::insertComponent(size_t index, ComponentPtr component) +{ + auto id = component->getID(); + if (id.isEmpty()) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Attempt to add a component with empty ID!"; + return; + } + if (d->componentIndex.contains(id)) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Attempt to add a component that is already present!"; + return; + } + beginInsertRows(QModelIndex(), static_cast(index), static_cast(index)); + d->components.insert(index, component); + d->componentIndex[id] = component; + endInsertRows(); + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + scheduleSave(); +} + +void PackProfile::componentDataChanged() +{ + auto objPtr = qobject_cast(sender()); + if (!objPtr) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "PackProfile got dataChanged signal from a non-Component!"; + return; + } + if (objPtr->getID() == "net.minecraft") + { + emit minecraftChanged(); + } + // figure out which one is it... in a seriously dumb way. + int index = 0; + for (auto component : d->components) + { + if (component.get() == objPtr) + { + emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + scheduleSave(); + return; + } + index++; + } + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "PackProfile got dataChanged signal from a Component which does not belong to it!"; +} + +bool PackProfile::remove(const int index) +{ + auto patch = getComponent(index); + if (!patch->isRemovable()) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "is non-removable"; + return false; + } + + if (!removeComponent_internal(patch)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "could not be removed"; + return false; + } + + beginRemoveRows(QModelIndex(), index, index); + d->components.removeAt(index); + d->componentIndex.remove(patch->getID()); + endRemoveRows(); + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +bool PackProfile::remove(const QString& id) +{ + int i = 0; + for (auto patch : d->components) + { + if (patch->getID() == id) + { + return remove(i); + } + i++; + } + return false; +} + +bool PackProfile::customize(int index) +{ + auto patch = getComponent(index); + if (!patch->isCustomizable()) + { + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "is not customizable"; + return false; + } + if (!patch->customize()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "could not be customized"; + return false; + } + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +bool PackProfile::revertToBase(int index) +{ + auto patch = getComponent(index); + if (!patch->isRevertible()) + { + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "is not revertible"; + return false; + } + if (!patch->revert()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "could not be reverted"; + return false; + } + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +ComponentPtr PackProfile::getComponent(const QString& id) +{ + auto iter = d->componentIndex.find(id); + if (iter == d->componentIndex.end()) + { + return nullptr; + } + return (*iter); +} + +ComponentPtr PackProfile::getComponent(size_t index) +{ + if (index >= static_cast(d->components.size())) + { + return nullptr; + } + return d->components[index]; +} + +QVariant PackProfile::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= d->components.size()) + return QVariant(); + + auto patch = d->components.at(row); + + switch (role) + { + case Qt::CheckStateRole: + { + if (column == NameColumn) + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + return QVariant(); + } + case Qt::DisplayRole: + { + switch (column) + { + case NameColumn: return patch->getName(); + case VersionColumn: + { + if (patch->isCustom()) + { + return QString("%1 (Custom)").arg(patch->getVersion()); + } + else + { + return patch->getVersion(); + } + } + default: return QVariant(); + } + } + case Qt::DecorationRole: + { + if (column == NameColumn) + { + auto severity = patch->getProblemSeverity(); + switch (severity) + { + case ProblemSeverity::Warning: return "warning"; + case ProblemSeverity::Error: return "error"; + default: return QVariant(); + } + } + return QVariant(); + } + } + return QVariant(); +} + +bool PackProfile::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) +{ + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + auto component = d->components[index.row()]; + if (component->setEnabled(!component->isEnabled())) + { + return true; + } + } + return false; +} + +QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) + { + if (role == Qt::DisplayRole) + { + switch (section) + { + case NameColumn: return tr("Name"); + case VersionColumn: return tr("Version"); + default: return QVariant(); + } + } + } + return QVariant(); +} + +// Note: This method intentionally uses no precision for row indices - +// items are indexed by position, not by any floating-point value +Qt::ItemFlags PackProfile::flags(const QModelIndex& index) const +{ + if (!index.isValid()) + { + return Qt::NoItemFlags; + } + + Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + int row = index.row(); + + if (row < 0 || row >= d->components.size()) + { + return Qt::NoItemFlags; + } + + auto patch = d->components.at(row); + // Components can only be toggled if they support disabling and the profile isn't locked + if (patch->canBeDisabled() && !d->interactionDisabled) + { + outFlags |= Qt::ItemIsUserCheckable; + } + return outFlags; +} + +int PackProfile::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : d->components.size(); +} + +int PackProfile::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +void PackProfile::move(const int index, const MoveDirection direction) +{ + int theirIndex; + if (direction == MoveUp) + { + theirIndex = index - 1; + } + else + { + theirIndex = index + 1; + } + + if (index < 0 || index >= d->components.size()) + return; + if (theirIndex >= rowCount()) + theirIndex = rowCount() - 1; + if (theirIndex == -1) + theirIndex = rowCount() - 1; + if (index == theirIndex) + return; + int togap = theirIndex > index ? theirIndex + 1 : theirIndex; + + auto from = getComponent(index); + auto to = getComponent(theirIndex); + + if (!from || !to || !to->isMoveable() || !from->isMoveable()) + { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); + d->components.swapItemsAt(index, theirIndex); + endMoveRows(); + invalidateLaunchProfile(); + scheduleSave(); +} + +void PackProfile::invalidateLaunchProfile() +{ + d->m_profile.reset(); +} + +void PackProfile::installJarMods(QStringList selectedFiles) +{ + installJarMods_internal(selectedFiles); +} + +void PackProfile::installCustomJar(QString selectedFile) +{ + installCustomJar_internal(selectedFile); +} + +bool PackProfile::installComponents(QStringList selectedFiles) +{ + const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + return false; + + bool result = true; + for (const QString& source : selectedFiles) + { + const QFileInfo sourceInfo(source); + + auto versionFile = ProfileUtils::parseJsonFile(sourceInfo, false); + const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json"); + + if (!QFile::copy(source, target)) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Component" << source << "could not be copied to target" << target; + result = false; + continue; + } + + appendComponent(makeShared(this, versionFile->uid, versionFile)); + } + + scheduleSave(); + invalidateLaunchProfile(); + + return result; +} + +void PackProfile::installAgents(QStringList selectedFiles) +{ + installAgents_internal(selectedFiles); +} + +bool PackProfile::installEmpty(const QString& uid, const QString& name) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto f = std::make_shared(); + f->name = name; + f->uid = uid; + f->version = "1"; + QString patchFileName = FS::PathCombine(patchDir, uid + ".json"); + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(makeShared(this, f->uid, f)); + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::removeComponent_internal(ComponentPtr patch) +{ + bool ok = true; + // first, remove the patch file. this ensures it's not used anymore + auto fileName = patch->getFilename(); + if (fileName.size()) + { + QFile patchFile(fileName); + if (patchFile.exists() && !patchFile.remove()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "File" << fileName + << "could not be removed because:" << patchFile.errorString(); + return false; + } + } + + // Generic local resource removal + // Handles jar mods, mods, and local libraries + auto removeLocalLibrary = [this](LibraryPtr lib, const QString& overridePath = QString()) -> bool + { + if (!lib->isLocal()) + { + return true; + } + QStringList output, temp1, temp2, temp3; + lib->getApplicableFiles(d->m_instance->runtimeContext(), output, temp1, temp2, temp3, overridePath); + for (const auto& file : output) + { + QFile f(file); + if (f.exists() && !f.remove()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "File" << file << "could not be removed because:" << f.errorString(); + return false; + } + } + return true; + }; + + auto vFile = patch->getVersionFile(); + if (vFile) + { + // Jar Mods + for (auto& lib : vFile->jarMods) + { + ok &= removeLocalLibrary(lib, d->m_instance->jarmodsPath().absolutePath()); + } + // Mods (Loader mods, generic mods) - Assuming they reside in 'mods' folder + QString modsPath = FS::PathCombine(d->m_instance->instanceRoot(), "mods"); + for (auto& lib : vFile->mods) + { + ok &= removeLocalLibrary(lib, modsPath); + } + // Local Libraries (in libraries folder) + for (auto& lib : vFile->libraries) + { + if (lib->isLocal()) + { + ok &= removeLocalLibrary(lib); + } + } + } + return ok; +} + +bool PackProfile::ensureInstallDirs(QString& patchDir, QString& libDir) +{ + patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + return false; + + libDir = d->m_instance->getLocalLibraryPath(); + if (!FS::ensureFolderPathExists(libDir)) + return false; + + return true; +} + +bool PackProfile::writeVersionFile(const QString& patchDir, const std::shared_ptr& versionFile) +{ + QString patchFileName = FS::PathCombine(patchDir, versionFile->uid + ".json"); + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for writing:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); + file.close(); + + appendComponent(makeShared(this, versionFile->uid, versionFile)); + return true; +} + +bool PackProfile::installJarMods_internal(QStringList filepaths) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + + if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir())) + { + return false; + } + + for (auto filepath : filepaths) + { + QFileInfo sourceInfo(filepath); + QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + QString target_filename = id + ".jar"; + QString target_id = "custom.jarmod." + id; + QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; + QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); + + QFileInfo targetInfo(finalPath); + Q_ASSERT(!targetInfo.exists()); + + if (!QFile::copy(sourceInfo.absoluteFilePath(), QFileInfo(finalPath).absoluteFilePath())) + { + return false; + } + + auto f = std::make_shared(); + auto jarMod = std::make_shared(); + jarMod->setRawName(GradleSpecifier("custom.jarmods:" + id + ":1")); + jarMod->setFilename(target_filename); + jarMod->setDisplayName(sourceInfo.completeBaseName()); + jarMod->setHint("local"); + f->jarMods.append(jarMod); + f->name = target_name; + f->uid = target_id; + QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(makeShared(this, f->uid, f)); + } + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::installCustomJar_internal(QString filepath) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + + QString libDir = d->m_instance->getLocalLibraryPath(); + if (!FS::ensureFolderPathExists(libDir)) + { + return false; + } + + auto specifier = GradleSpecifier("custom:customjar:1"); + QFileInfo sourceInfo(filepath); + QString target_filename = specifier.getFileName(); + QString target_id = specifier.artifactId(); + QString target_name = sourceInfo.completeBaseName() + " (custom jar)"; + QString finalPath = FS::PathCombine(libDir, target_filename); + + QFileInfo jarInfo(finalPath); + if (jarInfo.exists()) + { + if (!FS::deletePath(finalPath)) + { + return false; + } + } + if (!QFile::copy(filepath, finalPath)) + { + return false; + } + + auto f = std::make_shared(); + auto jarMod = std::make_shared(); + jarMod->setRawName(specifier); + jarMod->setDisplayName(sourceInfo.completeBaseName()); + jarMod->setHint("local"); + f->mainJar = jarMod; + f->name = target_name; + f->uid = target_id; + QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(makeShared(this, f->uid, f)); + + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::installAgents_internal(QStringList filepaths) +{ + QString patchDir, libDir; + if (!ensureInstallDirs(patchDir, libDir)) + return false; + + for (const QString& source : filepaths) + { + const QFileInfo sourceInfo(source); + const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + const QString targetBaseName = id + ".jar"; + const QString targetId = "custom.agent." + id; + const QString targetName = sourceInfo.completeBaseName() + " (agent)"; + const QString target = FS::PathCombine(libDir, targetBaseName); + + const QFileInfo targetInfo(target); + Q_ASSERT(!targetInfo.exists()); + + if (!QFile::copy(source, target)) + return false; + + auto versionFile = std::make_shared(); + + auto agent = std::make_shared(); + + agent->setRawName("custom.agents:" + id + ":1"); + agent->setFilename(targetBaseName); + agent->setDisplayName(sourceInfo.completeBaseName()); + agent->setHint("local"); + + versionFile->agents.append(std::make_shared(agent, QString())); + versionFile->name = targetName; + versionFile->uid = targetId; + + if (!writeVersionFile(patchDir, versionFile)) + return false; + } + + scheduleSave(); + invalidateLaunchProfile(); + + return true; +} + +std::shared_ptr PackProfile::getProfile() const +{ + if (!d->m_profile) + { + try + { + auto profile = std::make_shared(); + for (auto file : d->components) + { + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Applying" << file->getID() + << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); + file->applyTo(profile.get()); + } + d->m_profile = profile; + } + catch (const Exception& error) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Couldn't apply profile patches because: " << error.cause(); + } + } + return d->m_profile; +} + +bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) +{ + auto iter = d->componentIndex.find(uid); + if (iter != d->componentIndex.end()) + { + ComponentPtr component = *iter; + // set existing + if (component->revert()) + { + // set new version + auto oldVersion = component->getVersion(); + component->setVersion(version); + component->setImportant(important); + + if (important) + { + component->setUpdateAction(UpdateAction{ UpdateActionImportantChanged{ oldVersion } }); + resolve(Net::Mode::Online); + } + + return true; + } + return false; + } + else + { + // add new + auto component = makeShared(this, uid); + component->m_version = version; + component->m_important = important; + appendComponent(component); + return true; + } +} + +QString PackProfile::getComponentVersion(const QString& uid) const +{ + const auto iter = d->componentIndex.find(uid); + if (iter != d->componentIndex.end()) + { + return (*iter)->getVersion(); + } + return QString(); +} + +void PackProfile::disableInteraction(bool disable) +{ + if (d->interactionDisabled != disable) + { + d->interactionDisabled = disable; + auto size = d->components.size(); + if (size) + { + emit dataChanged(index(0), index(size - 1)); + } + } +} + +std::optional PackProfile::getModLoaders() +{ + ModPlatform::ModLoaderTypes result; + bool has_any_loader = false; + + QMapIterator i(Component::KNOWN_MODLOADERS); + + while (i.hasNext()) + { + i.next(); + if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) + { + result |= i.value().type; + has_any_loader = true; + } + } + + if (!has_any_loader) + return {}; + return result; +} + +std::optional PackProfile::getSupportedModLoaders() +{ + auto loadersOpt = getModLoaders(); + if (!loadersOpt.has_value()) + return loadersOpt; + auto loaders = loadersOpt.value(); + if (loaders & ModPlatform::Quilt) + loaders |= ModPlatform::Fabric; + if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge)) + loaders |= ModPlatform::Forge; + return loaders; +} + +QList PackProfile::getModLoadersList() +{ + QList result; + for (auto c : d->components) + { + if (c->isEnabled() && Component::KNOWN_MODLOADERS.contains(c->getID())) + { + result.append(Component::KNOWN_MODLOADERS[c->getID()].type); + } + } + + // Quilt provides Fabric compatibility for Minecraft versions < 1.22 + // This may change when Quilt drops official Fabric support in future versions + if (result.contains(ModPlatform::Quilt) && !result.contains(ModPlatform::Fabric)) + { + auto mcVersion = getComponentVersion("net.minecraft"); + Version minecraftVer(mcVersion); + // Assume Quilt maintains Fabric compat for versions before 1.22 + if (minecraftVer < Version("1.22")) + { + result.append(ModPlatform::Fabric); + } + } + if (getComponentVersion("net.minecraft") == "1.20.1" && result.contains(ModPlatform::NeoForge) + && !result.contains(ModPlatform::Forge)) + { + result.append(ModPlatform::Forge); + } + return result; +} diff --git a/archived/projt-launcher/launcher/minecraft/PackProfile.h b/archived/projt-launcher/launcher/minecraft/PackProfile.h new file mode 100644 index 0000000000..f8b83ab4c7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/PackProfile.h @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022-2023 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "Component.h" +#include "LaunchProfile.h" +#include "modplatform/ModIndex.h" +#include "net/Mode.h" + +class MinecraftInstance; +struct PackProfileData; +class ComponentUpdateTask; + +class PackProfile : public QAbstractListModel +{ + Q_OBJECT + friend ComponentUpdateTask; + + public: + enum Columns + { + NameColumn = 0, + VersionColumn, + NUM_COLUMNS + }; + + struct Result + { + bool success; + QString error; + bool changed = true; + + // Implicit conversion to bool + operator bool() const + { + return success; + } + + // Factory methods for convenience + static Result Success(bool changed = true) + { + return { true, "", changed }; + } + + static Result Error(const QString& errorMessage) + { + return { false, errorMessage, false }; + } + }; + + explicit PackProfile(MinecraftInstance* instance); + virtual ~PackProfile(); + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex& parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + + /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. + void buildingFromScratch(); + + /// install more jar mods + void installJarMods(QStringList selectedFiles); + + /// install a jar/zip as a replacement for the main jar + void installCustomJar(QString selectedFile); + + /// install MMC/Prism component files + bool installComponents(QStringList selectedFiles); + + /// install Java agent files + void installAgents(QStringList selectedFiles); + + enum MoveDirection + { + MoveUp, + MoveDown + }; + /// move component file # up or down the list + void move(int index, MoveDirection direction); + + /// remove component file # - including files/records + bool remove(int index); + + /// remove component file by id - including files/records + bool remove(const QString& id); + + bool customize(int index); + + bool revertToBase(int index); + + /// reload the list, reload all components, resolve dependencies + Result reload(Net::Mode netmode); + + // reload all components, resolve dependencies + void resolve(Net::Mode netmode); + + /// get current running task... + Task::Ptr getCurrentTask(); + + std::shared_ptr getProfile() const; + + // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config + void setOldConfigVersion(const QString& uid, const QString& version); + + QString getComponentVersion(const QString& uid) const; + + bool setComponentVersion(const QString& uid, const QString& version, bool important = false); + + bool installEmpty(const QString& uid, const QString& name); + + QString patchFilePathForUid(const QString& uid) const; + + /// if there is a save scheduled, do it now. + void saveNow(); + + /// helper method, returns RuntimeContext of instance + RuntimeContext runtimeContext(); + + signals: + void minecraftChanged(); + + public: + /// get the profile component by id + ComponentPtr getComponent(const QString& id); + + /// get the profile component by index + ComponentPtr getComponent(size_t index); + + /// Add the component to the internal list of patches. + /// Components are appended (not inserted) to preserve mod loader load order. + void appendComponent(ComponentPtr component); + + std::optional getModLoaders(); + // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) + std::optional getSupportedModLoaders(); + QList getModLoadersList(); + + /// apply the component patches. Catches all the errors and returns true/false for success/failure + void invalidateLaunchProfile(); + + private: + void scheduleSave(); + bool saveIsScheduled() const; + + /// insert component so that its index is ideally the specified one (returns real index) + void insertComponent(size_t index, ComponentPtr component); + + QString componentsFilePath() const; + QString patchesPattern() const; + + private slots: + void save_internal(); + void updateSucceeded(); + void updateFailed(const QString& error); + void componentDataChanged(); + void disableInteraction(bool disable); + + private: + Result load(); + + /// Helper function to ensure patch and library directories exist. + /// Returns true if both directories are ready, false on failure. + bool ensureInstallDirs(QString& patchDir, QString& libDir); + + /// Helper function to write a version file and register a component. + /// Returns true on success, false on failure. + bool writeVersionFile(const QString& patchDir, const std::shared_ptr& versionFile); + + bool installJarMods_internal(QStringList filepaths); + bool installCustomJar_internal(QString filepath); + bool installAgents_internal(QStringList filepaths); + bool removeComponent_internal(ComponentPtr patch); + + private: /* data */ + std::unique_ptr d; +}; diff --git a/archived/projt-launcher/launcher/minecraft/PackProfile_p.h b/archived/projt-launcher/launcher/minecraft/PackProfile_p.h new file mode 100644 index 0000000000..980acba3e1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/PackProfile_p.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include "Component.h" +#include "tasks/Task.h" + +class MinecraftInstance; +using ComponentContainer = QList; +using ComponentIndex = QMap; + +struct PackProfileData +{ + // the instance this belongs to + MinecraftInstance* m_instance; + + // the launch profile (volatile, temporary thing created on demand) + std::shared_ptr m_profile; + + // persistent list of components and related machinery + ComponentContainer components; + ComponentIndex componentIndex; + bool dirty = false; + QTimer m_saveTimer; + Task::Ptr m_updateTask; + bool loaded = false; + bool interactionDisabled = true; +}; diff --git a/archived/projt-launcher/launcher/minecraft/ParseUtils.cpp b/archived/projt-launcher/launcher/minecraft/ParseUtils.cpp new file mode 100644 index 0000000000..c092e1c3eb --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ParseUtils.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ParseUtils.h" +#include +#include +#include +#include + +QDateTime timeFromS3Time(QString str) +{ + return QDateTime::fromString(str, Qt::ISODate); +} + +QString timeToS3Time(QDateTime time) +{ + // this all because Qt can't format timestamps right. + int offsetRaw = time.offsetFromUtc(); + bool negative = offsetRaw < 0; + int offsetAbs = std::abs(offsetRaw); + + int offsetSeconds = offsetAbs % 60; + offsetAbs -= offsetSeconds; + + int offsetMinutes = offsetAbs % 3600; + offsetAbs -= offsetMinutes; + offsetMinutes /= 60; + + int offsetHours = offsetAbs / 3600; + + QString raw = time.toString("yyyy-MM-ddTHH:mm:ss"); + raw += (negative ? QChar('-') : QChar('+')); + raw += QString("%1").arg(offsetHours, 2, 10, QChar('0')); + raw += ":"; + raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0')); + return raw; +} diff --git a/archived/projt-launcher/launcher/minecraft/ParseUtils.h b/archived/projt-launcher/launcher/minecraft/ParseUtils.h new file mode 100644 index 0000000000..9db82fa016 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ParseUtils.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include + +/// take the timestamp used by S3 and turn it into QDateTime +QDateTime timeFromS3Time(QString str); + +/// take a timestamp and convert it into an S3 timestamp +QString timeToS3Time(QDateTime); diff --git a/archived/projt-launcher/launcher/minecraft/ProfileUtils.cpp b/archived/projt-launcher/launcher/minecraft/ProfileUtils.cpp new file mode 100644 index 0000000000..69ca5a5227 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ProfileUtils.cpp @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ProfileUtils.h" +#include +#include "Json.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/VersionFilterData.h" + +#include +#include +#include + +namespace ProfileUtils +{ + + static const int currentOrderFileVersion = 1; + + bool readOverrideOrders(QString path, PatchOrder& order) + { + QFile orderFile(path); + if (!orderFile.exists()) + { + qWarning() << "Order file doesn't exist. Ignoring."; + return false; + } + if (!orderFile.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << orderFile.fileName() << " for reading:" << orderFile.errorString(); + qWarning() << "Ignoring overridden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); + qWarning() << "Ignoring overridden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireInteger(obj.value("version")); + if (version != currentOrderFileVersion) + { + throw JSONValidationError( + QObject::tr("Invalid order file version, expected %1").arg(currentOrderFileVersion)); + } + auto orderArray = Json::requireArray(obj.value("order")); + for (auto item : orderArray) + { + order.append(Json::requireString(item)); + } + } + catch ([[maybe_unused]] const JSONValidationError& err) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; + qWarning() << "Ignoring overridden order"; + order.clear(); + return false; + } + return true; + } + + static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error) + { + auto outError = std::make_shared(); + outError->uid = outError->name = fileId; + // outError->filename = filepath; + outError->addProblem(ProblemSeverity::Error, error); + return outError; + } + + static VersionFilePtr guardedParseJson(const QJsonDocument& doc, + const QString& fileId, + const QString& filepath, + const bool& requireOrder) + { + try + { + return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder); + } + catch (const Exception& e) + { + return createErrorVersionFile(fileId, filepath, e.cause()); + } + } + + VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, const bool requireOrder) + { + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = + QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonParseError error; + auto data = file.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + file.close(); + if (error.error != QJsonParseError::NoError) + { + int line = 1; + int column = 0; + for (int i = 0; i < error.offset; i++) + { + if (data[i] == '\n') + { + line++; + column = 0; + continue; + } + column++; + } + auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.") + .arg(fileInfo.fileName(), error.errorString()) + .arg(line) + .arg(column); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); + } + + bool saveJsonFile(const QJsonDocument& doc, const QString& filename) + { + auto data = doc.toJson(); + QSaveFile jsonFile(filename); + if (!jsonFile.open(QIODevice::WriteOnly)) + { + jsonFile.cancelWriting(); + qWarning() << "Couldn't open" << filename << "for writing"; + return false; + } + jsonFile.write(data); + if (!jsonFile.commit()) + { + qWarning() << "Couldn't save" << filename; + return false; + } + return true; + } + + void removeLwjglFromPatch(VersionFilePtr patch) + { + auto filter = [](QList& libs) + { + QList filteredLibs; + for (auto lib : libs) + { + if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) + { + filteredLibs.append(lib); + } + } + libs = filteredLibs; + }; + filter(patch->libraries); + } +} // namespace ProfileUtils diff --git a/archived/projt-launcher/launcher/minecraft/ProfileUtils.h b/archived/projt-launcher/launcher/minecraft/ProfileUtils.h new file mode 100644 index 0000000000..ebfef4bf66 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ProfileUtils.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once +#include "Library.h" +#include "VersionFile.h" + +namespace ProfileUtils +{ + using PatchOrder = QStringList; + + /// Read and parse a OneSix format order file + bool readOverrideOrders(QString path, PatchOrder& order); + + /// Write a OneSix format order file + bool writeOverrideOrders(QString path, const PatchOrder& order); + + /// Parse a version file in JSON format + VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, bool requireOrder); + + /// Save a JSON file (in any format) + bool saveJsonFile(const QJsonDocument& doc, const QString& filename); + + /// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. + void removeLwjglFromPatch(VersionFilePtr patch); + +} // namespace ProfileUtils diff --git a/archived/projt-launcher/launcher/minecraft/Rule.cpp b/archived/projt-launcher/launcher/minecraft/Rule.cpp new file mode 100644 index 0000000000..1a21a04b9b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Rule.cpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2025 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include +#include + +#include "Rule.h" + +Rule Rule::fromJson(const QJsonObject& object) +{ + Rule result; + + if (object["action"] == "allow") + result.m_action = Allow; + else if (object["action"] == "disallow") + result.m_action = Disallow; + + if (auto os = object["os"]; os.isObject()) + { + if (auto name = os["name"].toString(); !name.isNull()) + { + result.m_os = OS{ + name, + os["version"].toString(), + }; + } + } + + return result; +} + +QJsonObject Rule::toJson() +{ + QJsonObject result; + + if (m_action == Allow) + result["action"] = "allow"; + else if (m_action == Disallow) + result["action"] = "disallow"; + + if (m_os.has_value()) + { + QJsonObject os; + + os["name"] = m_os->name; + + if (!m_os->version.isEmpty()) + os["version"] = m_os->version; + + result["os"] = os; + } + + return result; +} + +Rule::Action Rule::apply(const RuntimeContext& runtimeContext) +{ + if (m_os.has_value() && !runtimeContext.classifierMatches(m_os->name)) + return Defer; + + return m_action; +} diff --git a/archived/projt-launcher/launcher/minecraft/Rule.h b/archived/projt-launcher/launcher/minecraft/Rule.h new file mode 100644 index 0000000000..f3dd8fadf8 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Rule.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2025 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include "RuntimeContext.h" + +class Library; + +class Rule +{ + public: + enum Action + { + Allow, + Disallow, + Defer + }; + + static Rule fromJson(const QJsonObject& json); + QJsonObject toJson(); + + Action apply(const RuntimeContext& runtimeContext); + + private: + struct OS + { + QString name; + // NOTE: unsupported, but retained to avoid information loss + QString version; + }; + + Action m_action = Defer; + std::optional m_os; +}; diff --git a/archived/projt-launcher/launcher/minecraft/ShortcutUtils.cpp b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.cpp new file mode 100644 index 0000000000..dc795f94b6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.cpp @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ShortcutUtils.h" + +#include "FileSystem.h" + +#include +#include + +#include +#include +#include + +namespace ShortcutUtils +{ + + bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) + { + if (!shortcut.instance) + return false; + + QString appPath = QApplication::applicationFilePath(); + auto icon = + APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + QString iconPath; + QStringList args; +#if defined(Q_OS_MACOS) + if (appPath.startsWith("/private/var/")) + { + QMessageBox::critical( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr( + "The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return false; + } + + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for application.")); + return false; + } + + QIcon iconObj = icon->icon(); + bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS"); + iconFile.close(); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for application.")); + return false; + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (appPath.startsWith("/tmp/.mount_")) + { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) + { + QMessageBox::critical( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr( + "Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + else if (appPath.endsWith("/")) + { + appPath.chop(1); + } + } + + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + + if (DesktopServices::isFlatpak()) + { + appPath = "flatpak"; + args.append({ "run", BuildConfig.LAUNCHER_APPID }); + } + +#elif defined(Q_OS_WIN) + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); + + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but parent 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->logo(); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); + + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + +#else + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Not supported on your platform!")); + return false; +#endif + args.append({ "--launch", shortcut.instance->id() }); + args.append(shortcut.extraArgs); + + QString shortcutPath = FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath); + if (shortcutPath.isEmpty()) + { +#if not defined(Q_OS_MACOS) + iconFile.remove(); +#endif + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); + return false; + } + + shortcut.instance->registerShortcut({ shortcut.name, shortcutPath, shortcut.target }); + return true; + } + + bool createInstanceShortcutOnDesktop(const Shortcut& shortcut) + { + if (!shortcut.instance) + return false; + + QString desktopDir = FS::getDesktopDir(); + if (desktopDir.isEmpty()) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Couldn't find desktop?!")); + return false; + } + + QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); + return true; + } + + bool createInstanceShortcutInApplications(const Shortcut& shortcut) + { + if (!shortcut.instance) + return false; + + QString applicationsDir = FS::getApplicationsDir(); + if (applicationsDir.isEmpty()) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Couldn't find applications folder?!")); + return false; + } + +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) + applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); + + QDir applicationsDirQ(applicationsDir); + if (!applicationsDirQ.mkpath(".")) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create instances folder in applications folder!")); + return false; + } +#endif + + QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); + return true; + } + + bool createInstanceShortcutInOther(const Shortcut& shortcut) + { + if (!shortcut.instance) + return false; + + QString defaultedDir = FS::getDesktopDir(); +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString extension = ".desktop"; +#elif defined(Q_OS_WINDOWS) + QString extension = ".lnk"; +#else + QString extension = ""; +#endif + + QString shortcutFilePath = + FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(defaultedDir); + + shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, + QObject::tr("Create Shortcut"), + shortcutFilePath, + QObject::tr("Desktop Entries") + " (*" + extension + ")"); + if (shortcutFilePath.isEmpty()) + return false; // file dialog canceled by user + + if (shortcutFilePath.endsWith(extension)) + shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); + return true; + } + +} // namespace ShortcutUtils diff --git a/archived/projt-launcher/launcher/minecraft/ShortcutUtils.h b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.h new file mode 100644 index 0000000000..16b033e285 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once +#include "Application.h" + +#include +#include + +namespace ShortcutUtils +{ + /// A struct to hold parameters for creating a shortcut + struct Shortcut + { + BaseInstance* instance; + QString name; + QString targetString; + QWidget* parent = nullptr; + QStringList extraArgs = {}; + QString iconKey = ""; + ShortcutTarget target; + }; + + /// Create an instance shortcut on the specified file path + bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); + + /// Create an instance shortcut on the desktop + bool createInstanceShortcutOnDesktop(const Shortcut& shortcut); + + /// Create an instance shortcut in the Applications directory + bool createInstanceShortcutInApplications(const Shortcut& shortcut); + + /// Create an instance shortcut in other directories + bool createInstanceShortcutInOther(const Shortcut& shortcut); + +} // namespace ShortcutUtils diff --git a/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.cpp b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.cpp new file mode 100644 index 0000000000..a48c63132f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "VanillaInstanceCreationTask.h" + +#include + +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" + +VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version) + : InstanceCreationTask(), + m_version(std::move(version)), + m_using_loader(true), + m_loader(std::move(loader)), + m_loader_version(std::move(loader_version)) +{} + +std::unique_ptr VanillaCreationTask::createInstance() +{ + setStatus(tr("Creating instance from version %1").arg(m_version->name())); + + auto instance_settings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); + instance_settings->suspendSave(); + { + auto createdInstance = std::make_unique(m_globalSettings, instance_settings, m_stagingPath); + auto& inst = *createdInstance; + auto components = inst.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + if (m_using_loader) + components->setComponentVersion(m_loader, m_loader_version->descriptor()); + + inst.setName(name()); + inst.setIconKey(m_instIcon); + + instance_settings->resumeSave(); + return createdInstance; + } +} diff --git a/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.h b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.h new file mode 100644 index 0000000000..f6874fb051 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "InstanceCreationTask.h" + +#include + +class VanillaCreationTask final : public InstanceCreationTask +{ + Q_OBJECT + public: + VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) + {} + VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); + + std::unique_ptr createInstance() override; + + private: + // Version to update to / create of the instance. + BaseVersion::Ptr m_version; + + bool m_using_loader = false; + QString m_loader; + BaseVersion::Ptr m_loader_version; +}; diff --git a/archived/projt-launcher/launcher/minecraft/VersionFile.cpp b/archived/projt-launcher/launcher/minecraft/VersionFile.cpp new file mode 100644 index 0000000000..94f718dd45 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFile.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include +#include + +#include + +#include "ParseUtils.h" +#include "minecraft/Library.h" +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFile.h" + +#include + +static bool isMinecraftVersion(const QString& uid) +{ + return uid == "net.minecraft"; +} + +void VersionFile::applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext) +{ + // Only real Minecraft can set those. Don't let anything override them. + if (isMinecraftVersion(uid)) + { + profile->applyMinecraftVersion(version); + profile->applyMinecraftVersionType(type); + // HACK: ignore assets from other version files than Minecraft + // workaround for stupid assets issue caused by amazon: + // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ + profile->applyMinecraftAssets(mojangAssetIndex); + } + + profile->applyMainJar(mainJar); + profile->applyMainClass(mainClass); + profile->applyAppletClass(appletClass); + profile->applyMinecraftArguments(minecraftArguments); + profile->applyAddnJvmArguments(addnJvmArguments); + profile->applyTweakers(addTweakers); + profile->applyJarMods(jarMods); + profile->applyMods(mods); + profile->applyTraits(traits); + profile->applyCompatibleJavaMajors(compatibleJavaMajors); + profile->applyCompatibleJavaName(compatibleJavaName); + + for (auto library : libraries) + { + profile->applyLibrary(library, runtimeContext); + } + for (auto mavenFile : mavenFiles) + { + profile->applyMavenFile(mavenFile, runtimeContext); + } + for (auto agent : agents) + { + profile->applyAgent(agent, runtimeContext); + } + profile->applyProblemSeverity(getProblemSeverity()); +} diff --git a/archived/projt-launcher/launcher/minecraft/VersionFile.h b/archived/projt-launcher/launcher/minecraft/VersionFile.h new file mode 100644 index 0000000000..e51957b44a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFile.h @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include "Agent.h" +#include "Library.h" +#include "ProblemProvider.h" +#include "java/core/RuntimePackage.hpp" +#include "minecraft/Rule.h" + +class PackProfile; +class VersionFile; +class LaunchProfile; +struct MojangDownloadInfo; +struct MojangAssetIndexInfo; + +using VersionFilePtr = std::shared_ptr; +class VersionFile : public ProblemContainer +{ + friend class MojangVersionFormat; + friend class OneSixVersionFormat; + + public: /* methods */ + void applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext); + + public: /* data */ + /// ProjT Launcher: order hint for this version file if no explicit order is set + int order = 0; + + /// ProjT Launcher: human readable name of this package + QString name; + + /// ProjT Launcher: package ID of this package + QString uid; + + /// ProjT Launcher: version of this package + QString version; + + /// ProjT Launcher: DEPRECATED dependency on a Minecraft version + QString dependsOnMinecraftVersion; + + /// Mojang: DEPRECATED used to version the Mojang version format + int minimumLauncherVersion = -1; + + /// Mojang: DEPRECATED version of Minecraft this is + QString minecraftVersion; + + /// Mojang: class to launch Minecraft with + QString mainClass; + + /// ProjT Launcher: class to launch legacy Minecraft with (embed in a custom window) + QString appletClass; + + /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) + QString minecraftArguments; + + /// ProjT Launcher: Additional JVM launch arguments + QStringList addnJvmArguments; + + /// Mojang: list of compatible java majors + QList compatibleJavaMajors; + + /// Mojang: the name of recommended java version + QString compatibleJavaName; + + /// Mojang: type of the Minecraft version + QString type; + + /// Mojang: the time this version was actually released by Mojang + QDateTime releaseTime; + + /// Mojang: DEPRECATED the time this version was last updated by Mojang + QDateTime updateTime; + + /// Mojang: DEPRECATED asset group to be used with Minecraft + QString assets; + + /// ProjT Launcher: list of tweaker mod arguments for launchwrapper + QStringList addTweakers; + + /// Mojang: list of libraries to add to the version + QList libraries; + + /// ProjT Launcher: list of maven files to put in the libraries folder, but not in classpath + QList mavenFiles; + + /// ProjT Launcher: list of agents to add to JVM arguments + QList agents; + + /// The main jar (Minecraft version library, normally) + LibraryPtr mainJar; + + /// ProjT Launcher: list of attached traits of this version file - used to enable features + QSet traits; + + /// ProjT Launcher: list of jar mods added to this version + QList jarMods; + + /// ProjT Launcher: list of mods added to this version + QList mods; + + /** + * ProjT Launcher: set of packages this depends on + * NOTE: this is shared with the meta format!!! + */ + projt::meta::DependencySet m_requires; + + /** + * ProjT Launcher: set of packages this conflicts with + * NOTE: this is shared with the meta format!!! + */ + projt::meta::DependencySet conflicts; + + /// is volatile -- may be removed as soon as it is no longer needed by something else + bool m_volatile = false; + + QList runtimes; + + public: + // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. + QMap> mojangDownloads; + + // Mojang: extended asset index download information + std::shared_ptr mojangAssetIndex; +}; diff --git a/archived/projt-launcher/launcher/minecraft/VersionFilterData.cpp b/archived/projt-launcher/launcher/minecraft/VersionFilterData.cpp new file mode 100644 index 0000000000..7d9db5f7e6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFilterData.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "VersionFilterData.h" +#include "ParseUtils.h" + +VersionFilterData g_VersionFilterData = VersionFilterData(); + +VersionFilterData::VersionFilterData() +{ + // 1.3.* + auto libs13 = QList{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, + { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, + { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" } }; + + fmlLibsMapping["1.3.2"] = libs13; + + // 1.4.* + auto libs14 = QList{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, + { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, + { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" }, + { "bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb" } }; + + fmlLibsMapping["1.4"] = libs14; + fmlLibsMapping["1.4.1"] = libs14; + fmlLibsMapping["1.4.2"] = libs14; + fmlLibsMapping["1.4.3"] = libs14; + fmlLibsMapping["1.4.4"] = libs14; + fmlLibsMapping["1.4.5"] = libs14; + fmlLibsMapping["1.4.6"] = libs14; + fmlLibsMapping["1.4.7"] = libs14; + + // 1.5 + fmlLibsMapping["1.5"] = QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, + { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, + { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, + { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, + { "deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8" }, + { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; + + // 1.5.1 + fmlLibsMapping["1.5.1"] = + QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, + { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, + { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, + { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, + { "deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6" }, + { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; + + // 1.5.2 + fmlLibsMapping["1.5.2"] = + QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, + { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, + { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, + { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, + { "deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9" }, + { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; + + // don't use installers for those. + forgeInstallerBlacklist = QSet({ "1.5.2" }); + + // Legacy cutoff date (Minecraft 1.6.2) - used for determining core mods support + // Core mods were phased out after this version when Forge modernized + legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00"); + lwjglWhitelist = + QSet{ "net.java.jinput:jinput", "net.java.jinput:jinput-platform", "net.java.jutils:jutils", + "org.lwjgl.lwjgl:lwjgl", "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform" }; + + java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); + java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); + java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); +} diff --git a/archived/projt-launcher/launcher/minecraft/VersionFilterData.h b/archived/projt-launcher/launcher/minecraft/VersionFilterData.h new file mode 100644 index 0000000000..b984f5f362 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFilterData.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include +#include +#include + +struct FMLlib +{ + QString filename; + QString checksum; +}; + +struct VersionFilterData +{ + VersionFilterData(); + // mapping between minecraft versions and FML libraries required + QMap> fmlLibsMapping; + // set of minecraft versions for which using forge installers is blacklisted + QSet forgeInstallerBlacklist; + // no new versions below this date will be accepted from Mojang servers + QDateTime legacyCutoffDate; + // Libraries that belong to LWJGL + QSet lwjglWhitelist; + // release date of first version to require Java 8 (17w13a) + QDateTime java8BeginsDate; + // release data of first version to require Java 16 (21w19a) + QDateTime java16BeginsDate; + // release data of first version to require Java 17 (1.18 Pre Release 2) + QDateTime java17BeginsDate; +}; +extern VersionFilterData g_VersionFilterData; diff --git a/archived/projt-launcher/launcher/minecraft/World.cpp b/archived/projt-launcher/launcher/minecraft/World.cpp new file mode 100644 index 0000000000..1fc040db9c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/World.cpp @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "World.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "GZip.h" + +#include + +#include + +#include "FileSystem.h" +#include "PSaveFile.h" + +using std::nullopt; +using std::optional; + +GameType::GameType(std::optional original) : original(original) +{ + if (!original) + { + return; + } + switch (*original) + { + case 0: type = GameType::Survival; break; + case 1: type = GameType::Creative; break; + case 2: type = GameType::Adventure; break; + case 3: type = GameType::Spectator; break; + default: break; + } +} + +QString GameType::toTranslatedString() const +{ + switch (type) + { + case GameType::Survival: return QCoreApplication::translate("GameType", "Survival"); + case GameType::Creative: return QCoreApplication::translate("GameType", "Creative"); + case GameType::Adventure: return QCoreApplication::translate("GameType", "Adventure"); + case GameType::Spectator: return QCoreApplication::translate("GameType", "Spectator"); + default: break; + } + if (original) + { + return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); + } + return QCoreApplication::translate("GameType", "Undefined"); +} + +QString GameType::toLogString() const +{ + switch (type) + { + case GameType::Survival: return "Survival"; + case GameType::Creative: return "Creative"; + case GameType::Adventure: return "Adventure"; + case GameType::Spectator: return "Spectator"; + default: break; + } + if (original) + { + return QString("Unknown (%1)").arg(*original); + } + return "Undefined"; +} + +std::unique_ptr parseLevelDat(QByteArray data) +{ + QByteArray output; + if (!GZip::unzip(data, output)) + { + return nullptr; + } + std::istringstream foo(std::string(output.constData(), output.size())); + try + { + auto pair = nbt::io::read_compound(foo); + + if (pair.first != "") + return nullptr; + + if (pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch (const nbt::io::input_error& e) + { + qWarning() << "Unable to parse level.dat:" << e.what(); + return nullptr; + } +} + +QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) +{ + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val(s.str().data(), (int)s.str().size()); + return val; +} + +QString getDatFromFS(const QFileInfo& root, QString file) +{ + QDir worldDir(root.filePath()); + if (!root.isDir() || !worldDir.exists(file)) + { + return QString(); + } + return worldDir.absoluteFilePath(file); +} + +QString getLevelDatFromFS(const QFileInfo& file) +{ + return getDatFromFS(file, "level.dat"); +} + +QByteArray getDatDataFromFS(const QFileInfo& root, QString file) +{ + auto fullFilePath = getDatFromFS(root, file); + if (fullFilePath.isNull()) + { + return QByteArray(); + } + QFile f(fullFilePath); + if (!f.open(QIODevice::ReadOnly)) + { + return QByteArray(); + } + return f.readAll(); +} + +QByteArray getLevelDatDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "level.dat"); +} + +QByteArray getWorldGenDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "data/minecraft/world_gen_settings.dat"); +} + +bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) +{ + auto fullFilePath = getLevelDatFromFS(file); + if (fullFilePath.isNull()) + { + return false; + } + PSaveFile f(fullFilePath); + if (!f.open(QIODevice::WriteOnly)) + { + return false; + } + QByteArray compressed; + if (!GZip::zip(data, compressed)) + { + return false; + } + if (f.write(compressed) != compressed.size()) + { + f.cancelWriting(); + return false; + } + return f.commit(); +} + +World::World(const QFileInfo& file) +{ + repath(file); +} + +void World::repath(const QFileInfo& file) +{ + m_containerFile = file; + m_folderName = file.fileName(); + if (file.isFile() && file.suffix() == "zip") + { + m_iconFile = QString(); + readFromZip(file); + } + else if (file.isDir()) + { + QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); + if (assumedIconPath.exists()) + { + m_iconFile = assumedIconPath.absoluteFilePath(); + } + readFromFS(file); + } +} + +bool World::resetIcon() +{ + if (m_iconFile.isNull()) + { + return false; + } + if (QFile(m_iconFile).remove()) + { + m_iconFile = QString(); + return true; + } + return false; +} + +int64_t loadSeed(QByteArray data); + +void World::readFromFS(const QFileInfo& file) +{ + auto bytes = getLevelDatDataFromFS(file); + if (bytes.isEmpty()) + { + m_isValid = false; + return; + } + loadFromLevelDat(bytes); + m_levelDatTime = file.lastModified(); + if (m_randomSeed == 0) + { + auto worldGenBytes = getWorldGenDataFromFS(file); + if (!worldGenBytes.isEmpty()) + { + m_randomSeed = loadSeed(worldGenBytes); + } + } +} + +void World::readFromZip(const QFileInfo& file) +{ + QuaZip zip(file.absoluteFilePath()); + m_isValid = zip.open(QuaZip::mdUnzip); + if (!m_isValid) + { + return; + } + auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); + m_isValid = !location.isEmpty(); + if (!m_isValid) + { + return; + } + m_containerOffsetPath = location; + QuaZipFile zippedFile(&zip); + // read the install profile + m_isValid = zip.setCurrentFile(location + "level.dat"); + if (!m_isValid) + { + return; + } + m_isValid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if (!modTime.isValid()) + { + modTime = levelDatInfo.dateTime; + } + m_levelDatTime = modTime; + if (!m_isValid) + { + return; + } + loadFromLevelDat(zippedFile.readAll()); + zippedFile.close(); +} + +bool World::install(const QString& to, const QString& name) +{ + auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); + if (!FS::ensureFolderPathExists(finalPath)) + { + return false; + } + bool ok = false; + if (m_containerFile.isFile()) + { + QuaZip zip(m_containerFile.absoluteFilePath()); + if (!zip.open(QuaZip::mdUnzip)) + { + return false; + } + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); + } + else if (m_containerFile.isDir()) + { + QString from = m_containerFile.filePath(); + ok = FS::copy(from, finalPath)(); + } + + if (ok && !name.isEmpty() && m_actualName != name) + { + QFileInfo finalPathInfo(finalPath); + World newWorld(finalPathInfo); + if (newWorld.isValid()) + { + newWorld.rename(name); + } + } + return ok; +} + +bool World::rename(const QString& newName) +{ + if (m_containerFile.isFile()) + { + return false; + } + + auto data = getLevelDatDataFromFS(m_containerFile); + if (data.isEmpty()) + { + return false; + } + + auto worldData = parseLevelDat(data); + if (!worldData) + { + return false; + } + auto& val = worldData->at("Data"); + if (val.get_type() != nbt::tag_type::Compound) + { + return false; + } + auto& dataCompound = val.as(); + dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); + data = serializeLevelDat(worldData.get()); + + putLevelDatDataToFS(m_containerFile, data); + + m_actualName = newName; + + QDir parentDir(m_containerFile.absoluteFilePath()); + parentDir.cdUp(); + QFile container(m_containerFile.absoluteFilePath()); + auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); + container.rename(parentDir.absoluteFilePath(dirName)); + + return true; +} + +namespace +{ + + optional read_string(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::String) + { + return nullopt; + } + auto& tag_str = namedValue.as(); + return QString::fromStdString(tag_str.get()); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to string."; + return nullopt; + } + } + + optional read_long(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::Long) + { + return nullopt; + } + auto& tag_str = namedValue.as(); + return tag_str.get(); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to long."; + return nullopt; + } + } + + optional read_int(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::Int) + { + return nullopt; + } + auto& tag_str = namedValue.as(); + return tag_str.get(); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "Int NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to int."; + return nullopt; + } + } + + GameType read_gametype(nbt::value& parent, const char* name) + { + return GameType(read_int(parent, name)); + } + +} // namespace + +int64_t loadSeed(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) + { + return 0; + } + + nbt::value* valPtr = nullptr; + try + { + valPtr = &levelData->at("data"); + } + catch (const std::out_of_range&) + { + return 0; + } + nbt::value& val = *valPtr; + + try + { + return read_long(val, "seed").value_or(0); + } + catch (const std::out_of_range&) + { + } + return 0; +} + +void World::loadFromLevelDat(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) + { + m_isValid = false; + return; + } + + nbt::value* valPtr = nullptr; + try + { + valPtr = &levelData->at("Data"); + } + catch (const std::out_of_range& e) + { + qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); + m_isValid = false; + return; + } + nbt::value& val = *valPtr; + + m_isValid = val.get_type() == nbt::tag_type::Compound; + if (!m_isValid) + return; + + auto name = read_string(val, "LevelName"); + m_actualName = name ? *name : m_folderName; + + auto timestamp = read_long(val, "LastPlayed"); + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; + + m_gameType = read_gametype(val, "GameType"); + + optional randomSeed; + try + { + auto& WorldGen_val = val.at("WorldGenSettings"); + randomSeed = read_long(WorldGen_val, "seed"); + } + catch (const std::out_of_range&) + {} + if (!randomSeed) + { + randomSeed = read_long(val, "RandomSeed"); + } + m_randomSeed = randomSeed ? *randomSeed : 0; + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + if (randomSeed) + { + qDebug() << "Seed:" << *randomSeed; + } + qDebug() << "Size:" << m_size; + qDebug() << "GameType:" << m_gameType.toLogString(); +} + +bool World::replace(World& with) +{ + if (!destroy()) + return false; + bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); + if (success) + { + m_folderName = with.m_folderName; + m_containerFile.refresh(); + } + return success; +} + +bool World::destroy() +{ + if (!m_isValid) + return false; + + if (FS::trash(m_containerFile.filePath())) + return true; + + if (m_containerFile.isDir()) + { + QDir d(m_containerFile.filePath()); + return d.removeRecursively(); + } + else if (m_containerFile.isFile()) + { + QFile file(m_containerFile.absoluteFilePath()); + return file.remove(); + } + return true; +} + +bool World::operator==(const World& other) const +{ + return m_isValid == other.m_isValid && folderName() == other.folderName(); +} + +bool World::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool World::isMoreThanOneHardLink() const +{ + if (m_containerFile.isDir()) + { + return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1; + } + return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; +} + +void World::setSize(int64_t size) +{ + m_size = size; +} diff --git a/archived/projt-launcher/launcher/minecraft/World.h b/archived/projt-launcher/launcher/minecraft/World.h new file mode 100644 index 0000000000..9466bb55e2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/World.h @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once +#include +#include +#include + +struct GameType +{ + GameType() = default; + GameType(std::optional original); + + QString toTranslatedString() const; + QString toLogString() const; + + enum + { + Unknown = -1, + Survival, + Creative, + Adventure, + Spectator + } type = Unknown; + std::optional original; +}; + +class World +{ + public: + World(const QFileInfo& file); + QString folderName() const + { + return m_folderName; + } + QString name() const + { + return m_actualName; + } + QString iconFile() const + { + return m_iconFile; + } + int64_t bytes() const + { + return m_size; + } + QDateTime lastPlayed() const + { + return m_lastPlayed; + } + GameType gameType() const + { + return m_gameType; + } + int64_t seed() const + { + return m_randomSeed; + } + bool isValid() const + { + return m_isValid; + } + bool isOnFS() const + { + return m_containerFile.isDir(); + } + QFileInfo container() const + { + return m_containerFile; + } + // delete all the files of this world + bool destroy(); + // replace this world with a copy of the other + bool replace(World& with); + // change the world's filesystem path (used by world lists for *MAGIC* purposes) + void repath(const QFileInfo& file); + // remove the icon file, if any + bool resetIcon(); + + bool rename(const QString& to); + bool install(const QString& to, const QString& name = QString()); + + void setSize(int64_t size); + + // WEAK compare operator - used for replacing worlds + bool operator==(const World& other) const; + + auto isSymLink() const -> bool + { + return m_containerFile.isSymLink(); + } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in + * that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + bool isSymLinkUnder(const QString& instPath) const; + + bool isMoreThanOneHardLink() const; + + QString canonicalFilePath() const + { + return m_containerFile.canonicalFilePath(); + } + + private: + void readFromZip(const QFileInfo& file); + void readFromFS(const QFileInfo& file); + void loadFromLevelDat(QByteArray data); + + protected: + QFileInfo m_containerFile; + QString m_containerOffsetPath; + QString m_folderName; + QString m_actualName; + QString m_iconFile; + QDateTime m_levelDatTime; + QDateTime m_lastPlayed; + int64_t m_size; + int64_t m_randomSeed = 0; + GameType m_gameType; + bool m_isValid = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/WorldList.cpp b/archived/projt-launcher/launcher/minecraft/WorldList.cpp new file mode 100644 index 0000000000..f4f3c8424c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/WorldList.cpp @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "WorldList.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +WorldList::WorldList(const QString& dir, BaseInstance* instance) + : QAbstractListModel(), + m_instance(instance), + m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + m_isWatching = false; + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); +} + +void WorldList::startWatching() +{ + if (m_isWatching) + { + return; + } + update(); + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void WorldList::stopWatching() +{ + if (!m_isWatching) + { + return; + } + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool WorldList::update() +{ + if (!isValid()) + return false; + + QList newWorlds; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) + { + if (!entry.isDir()) + continue; + + World w(entry); + if (w.isValid()) + { + newWorlds.append(w); + } + } + beginResetModel(); + m_worlds.swap(newWorlds); + endResetModel(); + loadWorldsAsync(); + return true; +} + +void WorldList::directoryChanged(QString) +{ + update(); +} + +bool WorldList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +QString WorldList::instDirPath() const +{ + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} + +bool WorldList::deleteWorld(int index) +{ + if (index >= m_worlds.size() || index < 0) + return false; + World& m = m_worlds[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + m_worlds.removeAt(index); + endRemoveRows(); + emit changed(); + return true; + } + return false; +} + +bool WorldList::deleteWorlds(int first, int last) +{ + for (int i = first; i <= last; i++) + { + World& m = m_worlds[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); + endRemoveRows(); + emit changed(); + return true; +} + +bool WorldList::resetIcon(int row) +{ + if (row >= m_worlds.size() || row < 0) + return false; + World& m = m_worlds[row]; + if (m.resetIcon()) + { + emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); + return true; + } + return false; +} + +int WorldList::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : 5; +} + +QVariant WorldList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= m_worlds.size()) + return QVariant(); + + QLocale locale; + + auto& world = m_worlds[row]; + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: return world.name(); + + case GameModeColumn: return world.gameType().toTranslatedString(); + + case LastPlayedColumn: return world.lastPlayed(); + + case SizeColumn: return locale.formattedDataSize(world.bytes()); + + case InfoColumn: + if (world.isSymLinkUnder(instDirPath())) + { + return tr("This world is symbolically linked from elsewhere."); + } + if (world.isMoreThanOneHardLink()) + { + return tr("\nThis world is hard linked elsewhere."); + } + return ""; + default: return QVariant(); + } + + case Qt::UserRole: + if (column == SizeColumn) + return QVariant::fromValue(world.bytes()); + return data(index, Qt::DisplayRole); + + case Qt::ToolTipRole: + { + if (column == InfoColumn) + { + if (world.isSymLinkUnder(instDirPath())) + { + return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change " + "the original." + "\nCanonical Path: %1") + .arg(world.canonicalFilePath()); + } + if (world.isMoreThanOneHardLink()) + { + return tr( + "Warning: This world is hard linked elsewhere. Editing it will also change the original."); + } + } + return world.folderName(); + } + case ObjectRole: + { + return QVariant::fromValue((void*)&world); + } + case FolderRole: + { + return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName())); + } + case SeedRole: + { + return QVariant::fromValue(world.seed()); + } + case NameRole: + { + return world.name(); + } + case LastPlayedRole: + { + return world.lastPlayed(); + } + case SizeRole: + { + return QVariant::fromValue(world.bytes()); + } + case IconFileRole: + { + return world.iconFile(); + } + default: return QVariant(); + } +} + +QVariant WorldList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case NameColumn: return tr("Name"); + case GameModeColumn: return tr("Game Mode"); + case LastPlayedColumn: return tr("Last Played"); + case SizeColumn: + //: World size on disk + return tr("Size"); + case InfoColumn: + //: special warnings? + return tr("Info"); + default: return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: return tr("The name of the world."); + case GameModeColumn: return tr("Game mode of the world."); + case LastPlayedColumn: return tr("Date and time the world was last played."); + case SizeColumn: return tr("Size of the world on disk."); + case InfoColumn: return tr("Information and warnings about the world."); + default: return QVariant(); + } + default: return QVariant(); + } +} + +QStringList WorldList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const +{ + QList urls; + + for (auto idx : indexes) + { + if (idx.column() != 0) + continue; + + int row = idx.row(); + if (row < 0 || row >= this->m_worlds.size()) + continue; + + const World& world = m_worlds[row]; + + if (!world.isValid() || !world.isOnFS()) + continue; + + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); + } + + auto result = new QMimeData(); + result->setUrls(urls); + return result; +} + +Qt::ItemFlags WorldList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions WorldList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +Qt::DropActions WorldList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +void WorldList::installWorld(QFileInfo filename) +{ + qDebug() << "installing: " << filename.absoluteFilePath(); + World w(filename); + if (!w.isValid()) + { + return; + } + w.install(m_dir.absolutePath()); +} + +bool WorldList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = m_isWatching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + + QFileInfo worldInfo(filename); + + if (!m_dir.entryInfoList().contains(worldInfo)) + { + installWorld(worldInfo); + } + } + if (was_watching) + startWatching(); + return true; + } + return false; +} + +int64_t calculateWorldSize(const QFileInfo& file) +{ + if (file.isFile() && file.suffix() == "zip") + { + return file.size(); + } + else if (file.isDir()) + { + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) + { + it.next(); + total += it.fileInfo().size(); + } + return total; + } + return -1; +} + +void WorldList::loadWorldsAsync() +{ + for (int i = 0; i < m_worlds.size(); ++i) + { + auto file = m_worlds.at(i).container(); + int row = i; + QThreadPool::globalInstance()->start( + [this, file, row]() mutable + { + auto size = calculateWorldSize(file); + + QMetaObject::invokeMethod( + this, + [this, size, row, file]() + { + if (row < m_worlds.size() && m_worlds[row].container() == file) + { + m_worlds[row].setSize(size); + + // Notify views + QModelIndex modelIndex = index(row); + emit dataChanged(modelIndex, modelIndex, { SizeRole }); + } + }, + Qt::QueuedConnection); + }); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/WorldList.h b/archived/projt-launcher/launcher/minecraft/WorldList.h new file mode 100644 index 0000000000..0099fd3e5e --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/WorldList.h @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include "BaseInstance.h" +#include "minecraft/World.h" + +class QFileSystemWatcher; + +class WorldList : public QAbstractListModel +{ + Q_OBJECT + public: + enum Columns + { + NameColumn, + GameModeColumn, + LastPlayedColumn, + SizeColumn, + InfoColumn + }; + + enum Roles + { + ObjectRole = Qt::UserRole + 1, + FolderRole, + SeedRole, + NameRole, + GameModeRole, + LastPlayedRole, + SizeRole, + IconFileRole + }; + + WorldList(const QString& dir, BaseInstance* instance); + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const + { + return parent.isValid() ? 0 : static_cast(size()); + }; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex& parent) const; + + size_t size() const + { + return m_worlds.size(); + }; + bool empty() const + { + return size() == 0; + } + World& operator[](size_t index) + { + return m_worlds[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /// Install a world from location + void installWorld(QFileInfo filename); + + /// Deletes the mod at the given index. + virtual bool deleteWorld(int index); + + /// Removes the world icon, if any + virtual bool resetIcon(int index); + + /// Deletes all the selected mods + virtual bool deleteWorlds(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + /// get data for drag action + virtual QMimeData* mimeData(const QModelIndexList& indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() const + { + return m_dir; + } + + QString instDirPath() const; + + const QList& allWorlds() const + { + return m_worlds; + } + + private slots: + void directoryChanged(QString path); + void loadWorldsAsync(); + + signals: + void changed(); + + protected: + BaseInstance* m_instance; + QFileSystemWatcher* m_watcher; + bool m_isWatching; + QDir m_dir; + QList m_worlds; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountData.cpp b/archived/projt-launcher/launcher/minecraft/auth/AccountData.cpp new file mode 100644 index 0000000000..7a6bce3ffc --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountData.cpp @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "AccountData.hpp" +#include +#include +#include +#include +#include + +namespace +{ + void tokenToJSONV3(QJsonObject& parent, const Token& t, const char* tokenName) + { + if (!t.persistent) + { + return; + } + QJsonObject out; + if (t.issueInstant.isValid()) + { + out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); + } + + if (t.notAfter.isValid()) + { + out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); + } + + bool save = false; + if (!t.token.isEmpty()) + { + out["token"] = QJsonValue(t.token); + save = true; + } + if (!t.refresh_token.isEmpty()) + { + out["refresh_token"] = QJsonValue(t.refresh_token); + save = true; + } + if (t.extra.size()) + { + out["extra"] = QJsonObject::fromVariantMap(t.extra); + save = true; + } + if (save) + { + parent[tokenName] = out; + } + } + + Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName) + { + Token out; + auto tokenObject = parent.value(tokenName).toObject(); + if (tokenObject.isEmpty()) + { + return out; + } + auto issueInstant = tokenObject.value("iat"); + if (issueInstant.isDouble()) + { + out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t)issueInstant.toDouble()) * 1000); + } + + auto notAfter = tokenObject.value("exp"); + if (notAfter.isDouble()) + { + out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t)notAfter.toDouble()) * 1000); + } + + auto token = tokenObject.value("token"); + if (token.isString()) + { + out.token = token.toString(); + out.validity = Validity::Assumed; + } + + auto refresh_token = tokenObject.value("refresh_token"); + if (refresh_token.isString()) + { + out.refresh_token = refresh_token.toString(); + } + + auto extra = tokenObject.value("extra"); + if (extra.isObject()) + { + out.extra = extra.toObject().toVariantMap(); + } + return out; + } + + void profileToJSONV3(QJsonObject& parent, MinecraftProfile p, const char* tokenName) + { + if (p.id.isEmpty()) + { + return; + } + QJsonObject out; + out["id"] = QJsonValue(p.id); + out["name"] = QJsonValue(p.name); + if (!p.currentCape.isEmpty()) + { + out["cape"] = p.currentCape; + } + + { + QJsonObject skinObj; + skinObj["id"] = p.skin.id; + skinObj["url"] = p.skin.url; + skinObj["variant"] = p.skin.variant; + if (p.skin.data.size()) + { + skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); + } + out["skin"] = skinObj; + } + + QJsonArray capesArray; + for (auto& cape : p.capes) + { + QJsonObject capeObj; + capeObj["id"] = cape.id; + capeObj["url"] = cape.url; + capeObj["alias"] = cape.alias; + if (cape.data.size()) + { + capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); + } + capesArray.push_back(capeObj); + } + out["capes"] = capesArray; + parent[tokenName] = out; + } + + MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenName) + { + MinecraftProfile out; + auto tokenObject = parent.value(tokenName).toObject(); + if (tokenObject.isEmpty()) + { + return out; + } + { + auto idV = tokenObject.value("id"); + auto nameV = tokenObject.value("name"); + if (!idV.isString() || !nameV.isString()) + { + qWarning() << "mandatory profile attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.name = nameV.toString(); + out.id = idV.toString(); + } + + { + auto skinV = tokenObject.value("skin"); + if (!skinV.isObject()) + { + qWarning() << "skin is missing"; + return MinecraftProfile(); + } + auto skinObj = skinV.toObject(); + auto idV = skinObj.value("id"); + auto urlV = skinObj.value("url"); + auto variantV = skinObj.value("variant"); + if (!idV.isString() || !urlV.isString() || !variantV.isString()) + { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.skin.id = idV.toString(); + out.skin.url = urlV.toString(); + out.skin.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + out.skin.variant = variantV.toString(); + + // data for skin is optional + auto dataV = skinObj.value("data"); + if (dataV.isString()) + { + auto base64Result = QByteArray::fromBase64Encoding(dataV.toString().toLatin1()); + if (base64Result.decodingStatus != QByteArray::Base64DecodingStatus::Ok) + { + qWarning() << "skin data is not valid base64"; + return MinecraftProfile(); + } + out.skin.data = base64Result.decoded; + } + else if (!dataV.isUndefined()) + { + qWarning() << "skin data is something unexpected"; + return MinecraftProfile(); + } + } + + { + auto capesV = tokenObject.value("capes"); + if (!capesV.isArray()) + { + qWarning() << "capes is not an array!"; + return MinecraftProfile(); + } + auto capesArray = capesV.toArray(); + for (auto capeV : capesArray) + { + if (!capeV.isObject()) + { + qWarning() << "cape is not an object!"; + return MinecraftProfile(); + } + auto capeObj = capeV.toObject(); + auto idV = capeObj.value("id"); + auto urlV = capeObj.value("url"); + auto aliasV = capeObj.value("alias"); + if (!idV.isString() || !urlV.isString() || !aliasV.isString()) + { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + Cape cape; + cape.id = idV.toString(); + cape.url = urlV.toString(); + cape.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + cape.alias = aliasV.toString(); + + // data for cape is optional. + auto dataV = capeObj.value("data"); + if (dataV.isString()) + { + auto base64Result = QByteArray::fromBase64Encoding(dataV.toString().toLatin1()); + if (base64Result.decodingStatus != QByteArray::Base64DecodingStatus::Ok) + { + qWarning() << "cape data is not valid base64"; + return MinecraftProfile(); + } + cape.data = base64Result.decoded; + } + else if (!dataV.isUndefined()) + { + qWarning() << "cape data is something unexpected"; + return MinecraftProfile(); + } + out.capes[cape.id] = cape; + } + } + // current cape + { + auto capeV = tokenObject.value("cape"); + if (capeV.isString()) + { + auto currentCape = capeV.toString(); + if (out.capes.contains(currentCape)) + { + out.currentCape = currentCape; + } + } + } + out.validity = Validity::Assumed; + return out; + } + + void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p) + { + if (p.validity == Validity::None) + { + return; + } + QJsonObject out; + out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft); + out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft); + parent["entitlement"] = out; + } + + bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) + { + auto entitlementObject = parent.value("entitlement").toObject(); + if (entitlementObject.isEmpty()) + { + return false; + } + { + auto ownsMinecraftV = entitlementObject.value("ownsMinecraft"); + auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft"); + if (!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) + { + qWarning() << "mandatory attributes are missing or of unexpected type"; + return false; + } + out.canPlayMinecraft = canPlayMinecraftV.toBool(false); + out.ownsMinecraft = ownsMinecraftV.toBool(false); + out.validity = Validity::Assumed; + } + return true; + } + +} // namespace + +bool AccountData::resumeStateFromV3(QJsonObject data) +{ + auto typeV = data.value("type"); + if (!typeV.isString()) + { + qWarning() << "Failed to parse account data: type is missing."; + return false; + } + auto typeS = typeV.toString(); + if (typeS == "MSA") + { + type = AccountType::MSA; + } + else if (typeS == "Offline") + { + type = AccountType::Offline; + } + else + { + qWarning() << "Failed to parse account data: type is not recognized."; + return false; + } + + if (type == AccountType::MSA) + { + auto clientIDV = data.value("msa-client-id"); + if (clientIDV.isString()) + { + msaClientID = clientIDV.toString(); + } // leave msaClientID empty if it doesn't exist or isn't a string + msaToken = tokenFromJSONV3(data, "msa"); + userToken = tokenFromJSONV3(data, "utoken"); + xboxApiToken = tokenFromJSONV3(data, "xrp-main"); + mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); + } + + yggdrasilToken = tokenFromJSONV3(data, "ygg"); + // versions before 7.2 used "offline" as the offline token + if (yggdrasilToken.token == "offline") + yggdrasilToken.token = "0"; + + minecraftProfile = profileFromJSONV3(data, "profile"); + if (!entitlementFromJSONV3(data, minecraftEntitlement)) + { + if (minecraftProfile.validity != Validity::None) + { + minecraftEntitlement.canPlayMinecraft = true; + minecraftEntitlement.ownsMinecraft = true; + minecraftEntitlement.validity = Validity::Assumed; + } + } + + validity_ = minecraftProfile.validity; + return true; +} + +QJsonObject AccountData::saveState() const +{ + QJsonObject output; + if (type == AccountType::MSA) + { + output["type"] = "MSA"; + output["msa-client-id"] = msaClientID; + tokenToJSONV3(output, msaToken, "msa"); + tokenToJSONV3(output, userToken, "utoken"); + tokenToJSONV3(output, xboxApiToken, "xrp-main"); + tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); + } + else if (type == AccountType::Offline) + { + output["type"] = "Offline"; + } + + tokenToJSONV3(output, yggdrasilToken, "ygg"); + profileToJSONV3(output, minecraftProfile, "profile"); + entitlementToJSONV3(output, minecraftEntitlement); + return output; +} + +QString AccountData::accessToken() const +{ + return yggdrasilToken.token; +} + +QString AccountData::profileId() const +{ + return minecraftProfile.id; +} + +QString AccountData::profileName() const +{ + if (minecraftProfile.name.size() == 0) + { + return QObject::tr("No profile (%1)").arg(accountDisplayString()); + } + else + { + return minecraftProfile.name; + } +} + +QString AccountData::accountDisplayString() const +{ + switch (type) + { + case AccountType::Offline: + { + return QObject::tr(""); + } + case AccountType::MSA: + { + if (xboxApiToken.extra.contains("gtg")) + { + return xboxApiToken.extra["gtg"].toString(); + } + return "Xbox profile missing"; + } + default: + { + return "Invalid Account"; + } + } +} + +QString AccountData::lastError() const +{ + return errorString; +} diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountData.hpp b/archived/projt-launcher/launcher/minecraft/auth/AccountData.hpp new file mode 100644 index 0000000000..f30d77225f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountData.hpp @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once +#include +#include +#include +#include + +#include +#include +#include +#include + +enum class Validity +{ + None, + Assumed, + Certain +}; + +struct Token +{ + QDateTime issueInstant; + QDateTime notAfter; + QString token; + QString refresh_token; + QVariantMap extra; + + Validity validity = Validity::None; + bool persistent = true; +}; + +struct Skin +{ + QString id; + QString url; + QString variant; + + QByteArray data; +}; + +struct Cape +{ + QString id; + QString url; + QString alias; + + QByteArray data; +}; + +struct MinecraftEntitlement +{ + bool ownsMinecraft = false; + bool canPlayMinecraft = false; + Validity validity = Validity::None; +}; + +struct MinecraftProfile +{ + QString id; + QString name; + Skin skin; + QString currentCape; + QMap capes; + Validity validity = Validity::None; +}; + +enum class AccountType +{ + MSA, + Offline +}; + +enum class AccountState +{ + Unchecked, + Offline, + Working, + Online, + Disabled, + Errored, + Expired, + Gone +}; + +/** + * State of an authentication task. + * Used by AuthFlow to communicate progress and results. + */ +enum class AccountTaskState +{ + STATE_CREATED, + STATE_WORKING, + STATE_SUCCEEDED, + STATE_OFFLINE, + STATE_DISABLED, + STATE_FAILED_SOFT, + STATE_FAILED_HARD, + STATE_FAILED_GONE +}; + +struct AccountData +{ + QJsonObject saveState() const; + bool resumeStateFromV3(QJsonObject data); + + //! userName for Mojang accounts, gamertag for MSA + QString accountDisplayString() const; + + //! Yggdrasil access token, as passed to the game. + QString accessToken() const; + + QString profileId() const; + QString profileName() const; + + QString lastError() const; + + AccountType type = AccountType::MSA; + + QString msaClientID; + Token msaToken; + Token userToken; + Token xboxApiToken; + Token mojangservicesToken; + + Token yggdrasilToken; + MinecraftProfile minecraftProfile; + MinecraftEntitlement minecraftEntitlement; + Validity validity_ = Validity::None; + + // runtime only information (not saved with the account) + QString internalId; + QString errorString; + AccountState accountState = AccountState::Unchecked; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountList.cpp b/archived/projt-launcher/launcher/minecraft/auth/AccountList.cpp new file mode 100644 index 0000000000..c68d214aad --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountList.cpp @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "AccountList.hpp" +#include "AccountData.hpp" +#include "tasks/Task.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +enum AccountListVersion +{ + MojangMSA = 3 +}; + +AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) +{ + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); + m_nextTimer = new QTimer(this); + m_nextTimer->setSingleShot(true); + connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); +} + +AccountList::~AccountList() noexcept +{} + +int AccountList::findAccountByProfileId(const QString& profileId) const +{ + for (int i = 0; i < count(); i++) + { + MinecraftAccountPtr account = at(i); + if (account->profileId() == profileId) + { + return i; + } + } + return -1; +} + +MinecraftAccountPtr AccountList::getAccountByProfileName(const QString& profileName) const +{ + for (int i = 0; i < count(); i++) + { + MinecraftAccountPtr account = at(i); + if (account->profileName() == profileName) + { + return account; + } + } + return nullptr; +} + +const MinecraftAccountPtr AccountList::at(int i) const +{ + return MinecraftAccountPtr(m_accounts.at(i)); +} + +QStringList AccountList::profileNames() const +{ + QStringList out; + for (auto& account : m_accounts) + { + auto profileName = account->profileName(); + if (profileName.isEmpty()) + { + continue; + } + out.append(profileName); + } + return out; +} + +void AccountList::addAccount(const MinecraftAccountPtr account) +{ + // NOTE: Do not allow adding something that's already there. We shouldn't let it continue + // because of the signal / slot connections after this. + if (m_accounts.contains(account)) + { + qDebug() << "Tried to add account that's already on the accounts list!"; + return; + } + + // hook up notifications for changes in the account + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + + // override/replace existing account with the same profileId + auto profileId = account->profileId(); + if (profileId.size()) + { + auto existingAccount = findAccountByProfileId(profileId); + if (existingAccount != -1) + { + qDebug() << "Replacing old account with a new one with the same profile ID!"; + + MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; + m_accounts[existingAccount] = account; + if (m_defaultAccount == existingAccountPtr) + { + m_defaultAccount = account; + } + // disconnect notifications for changes in the account being replaced + existingAccountPtr->disconnect(this); + emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); + onListChanged(); + return; + } + } + + // if we don't have this profileId yet, add the account to the end + int row = m_accounts.count(); + qDebug() << "Inserting account at index" << row; + + beginInsertRows(QModelIndex(), row, row); + m_accounts.append(account); + endInsertRows(); + + onListChanged(); +} + +void AccountList::removeAccount(QModelIndex index) +{ + int row = index.row(); + if (index.isValid() && row >= 0 && row < m_accounts.size()) + { + auto& account = m_accounts[row]; + if (account == m_defaultAccount) + { + m_defaultAccount = nullptr; + onDefaultAccountChanged(); + } + account->disconnect(this); + + beginRemoveRows(QModelIndex(), row, row); + m_accounts.removeAt(index.row()); + endRemoveRows(); + onListChanged(); + } +} + +MinecraftAccountPtr AccountList::defaultAccount() const +{ + return m_defaultAccount; +} + +void AccountList::setDefaultAccount(MinecraftAccountPtr newAccount) +{ + if (!newAccount && m_defaultAccount) + { + int idx = 0; + auto previousDefaultAccount = m_defaultAccount; + m_defaultAccount = nullptr; + for (MinecraftAccountPtr account : m_accounts) + { + if (account == previousDefaultAccount) + { + emit dataChanged(index(idx), index(idx, columnCount(QModelIndex()) - 1)); + } + idx++; + } + onDefaultAccountChanged(); + } + else + { + auto currentDefaultAccount = m_defaultAccount; + int currentDefaultAccountIdx = -1; + auto newDefaultAccount = m_defaultAccount; + int newDefaultAccountIdx = -1; + int idx = 0; + for (MinecraftAccountPtr account : m_accounts) + { + if (account == newAccount) + { + newDefaultAccount = account; + newDefaultAccountIdx = idx; + } + if (currentDefaultAccount == account) + { + currentDefaultAccountIdx = idx; + } + idx++; + } + if (currentDefaultAccount != newDefaultAccount) + { + emit dataChanged(index(currentDefaultAccountIdx), + index(currentDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + emit dataChanged(index(newDefaultAccountIdx), index(newDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + m_defaultAccount = newDefaultAccount; + onDefaultAccountChanged(); + } + } +} + +void AccountList::accountChanged() +{ + // the list changed. there is no doubt. + onListChanged(); +} + +void AccountList::accountActivityChanged(bool active) +{ + MinecraftAccount* account = qobject_cast(sender()); + bool found = false; + for (int i = 0; i < count(); i++) + { + if (at(i).get() == account) + { + emit dataChanged(index(i), index(i, columnCount(QModelIndex()) - 1)); + found = true; + break; + } + } + if (found) + { + emit listActivityChanged(); + if (active) + { + beginActivity(); + } + else + { + endActivity(); + } + } +} + +void AccountList::onListChanged() +{ + if (m_autosave) + { + if (!saveList()) + { + qWarning() << "Failed to save account list automatically"; + emit fileSaveFailed(m_listFilePath); + } + } + + emit listChanged(); +} + +void AccountList::onDefaultAccountChanged() +{ + if (m_autosave) + saveList(); + + emit defaultAccountChanged(); +} + +int AccountList::count() const +{ + return m_accounts.count(); +} + +QString getAccountStatus(AccountState status) +{ + switch (status) + { + case AccountState::Unchecked: return QObject::tr("Unchecked", "Account status"); + case AccountState::Offline: return QObject::tr("Offline", "Account status"); + case AccountState::Online: return QObject::tr("Ready", "Account status"); + case AccountState::Working: return QObject::tr("Working", "Account status"); + case AccountState::Errored: return QObject::tr("Errored", "Account status"); + case AccountState::Expired: return QObject::tr("Expired", "Account status"); + case AccountState::Disabled: return QObject::tr("Disabled", "Account status"); + case AccountState::Gone: return QObject::tr("Gone", "Account status"); + default: return QObject::tr("Unknown", "Account status"); + } +} + +QVariant AccountList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + MinecraftAccountPtr account = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case IconColumn: return QVariant(); // Icons are handled by DecorationRole + case ProfileNameColumn: return account->profileName(); + case NameColumn: return account->accountDisplayString(); + case TypeColumn: + { + switch (account->accountType()) + { + case AccountType::MSA: + { + return tr("MSA", "Account type"); + } + case AccountType::Offline: + { + return tr("Offline", "Account type"); + } + } + return tr("Unknown", "Account type"); + } + case StatusColumn: return getAccountStatus(account->accountState()); + default: return QVariant(); + } + + case Qt::DecorationRole: + if (index.column() == IconColumn) + { + return account->getFace(); + } + return QVariant(); + + case Qt::ToolTipRole: return account->accountDisplayString(); + + case PointerRole: return QVariant::fromValue(account); + + case Qt::CheckStateRole: + if (index.column() == ProfileNameColumn) + return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; + return QVariant(); + + default: return QVariant(); + } +} + +QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case IconColumn: return QVariant(); // No header text for icon column + case ProfileNameColumn: return tr("Username"); + case NameColumn: return tr("Account"); + case TypeColumn: return tr("Type"); + case StatusColumn: return tr("Status"); + default: return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case IconColumn: return tr("Account avatar"); + case ProfileNameColumn: return tr("Minecraft username associated with the account."); + case NameColumn: return tr("User name of the account."); + case TypeColumn: return tr("Type of the account (MSA or Offline)"); + case StatusColumn: return tr("Current status of the account."); + default: return QVariant(); + } + + default: return QVariant(); + } +} + +int AccountList::rowCount(const QModelIndex& parent) const +{ + // Return count + return parent.isValid() ? 0 : count(); +} + +int AccountList::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Qt::ItemFlags AccountList::flags(const QModelIndex& index) const +{ + if (index.row() < 0 || index.row() >= rowCount(index.parent()) || !index.isValid()) + { + return Qt::NoItemFlags; + } + + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()) || !idx.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + if (value == Qt::Checked) + { + MinecraftAccountPtr account = at(idx.row()); + setDefaultAccount(account); + } + else if (m_defaultAccount == at(idx.row())) + setDefaultAccount(nullptr); + } + + emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); + return true; +} + +bool AccountList::loadList() +{ + if (m_listFilePath.isEmpty()) + { + qCritical() << "Can't load Mojang account list. No file path given and no default set."; + return false; + } + + QFile file(m_listFilePath); + + // Try to open the file and fail if we can't. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << QString("Failed to read the account list file (%1): %2") + .arg(m_listFilePath, file.errorString()) + .toUtf8(); + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse account list file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid account list JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Make sure the format version matches. + auto listVersion = root.value("formatVersion").toVariant().toInt(); + if (listVersion == AccountListVersion::MojangMSA) + return loadV3(root); + + QString newName = "accounts-old.json"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; +} + +bool AccountList::loadV3(QJsonObject& root) +{ + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj); + if (account.get() != nullptr) + { + auto profileId = account->profileId(); + if (profileId.size()) + { + if (findAccountByProfileId(profileId) != -1) + { + continue; + } + } + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + m_accounts.append(account); + if (accountObj.value("active").toBool(false)) + { + m_defaultAccount = account; + } + } + else + { + qWarning() << "Failed to load an account."; + } + } + endResetModel(); + return true; +} + +bool AccountList::saveList() +{ + if (m_listFilePath.isEmpty()) + { + qCritical() << "Can't save Mojang account list. No file path given and no default set."; + return false; + } + + // make sure the parent folder exists + if (!FS::ensureFilePathExists(m_listFilePath)) + return false; + + // make sure the file wasn't overwritten with a folder before (fixes a bug) + QFileInfo finfo(m_listFilePath); + if (finfo.isDir()) + { + QDir badDir(m_listFilePath); + badDir.removeRecursively(); + } + + qDebug() << "Writing account list to" << m_listFilePath; + + qDebug() << "Building JSON data structure."; + // Build the JSON document to write to the list file. + QJsonObject root; + + root.insert("formatVersion", AccountListVersion::MojangMSA); + + // Build a list of accounts. + qDebug() << "Building account array."; + QJsonArray accounts; + for (MinecraftAccountPtr account : m_accounts) + { + QJsonObject accountObj = account->saveToJson(); + if (m_defaultAccount == account) + { + accountObj["active"] = true; + } + accounts.append(accountObj); + } + + // Insert the account list into the root object. + root.insert("accounts", accounts); + + // Create a JSON document object to convert our JSON to bytes. + QJsonDocument doc(root); + + // Now that we're done building the JSON object, we can write it to the file. + qDebug() << "Writing account list to file."; + QSaveFile file(m_listFilePath); + + // Try to open the file and fail if we can't. + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << QString("Failed to write the account list file (%1): %2") + .arg(m_listFilePath, file.errorString()) + .toUtf8(); + return false; + } + + // Write the JSON to the file. + file.write(doc.toJson()); + file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); + if (file.commit()) + { + qDebug() << "Saved account list to" << m_listFilePath; + return true; + } + else + { + qDebug() << "Failed to save accounts to" << m_listFilePath; + return false; + } +} + +void AccountList::setListFilePath(QString path, bool autosave) +{ + m_listFilePath = path; + m_autosave = autosave; +} + +bool AccountList::anyAccountIsValid() +{ + for (auto account : m_accounts) + { + if (account->ownsMinecraft()) + { + return true; + } + } + return false; +} + +void AccountList::fillQueue() +{ + if (m_defaultAccount && m_defaultAccount->shouldRefresh()) + { + auto idToRefresh = m_defaultAccount->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first"; + } + + for (int i = 0; i < count(); i++) + { + auto account = at(i); + if (account == m_defaultAccount) + { + continue; + } + + if (account->shouldRefresh()) + { + auto idToRefresh = account->internalId(); + queueRefresh(idToRefresh); + } + } + tryNext(); +} + +void AccountList::requestRefresh(QString accountId) +{ + auto index = m_refreshQueue.indexOf(accountId); + if (index != -1) + { + m_refreshQueue.removeAt(index); + } + m_refreshQueue.push_front(accountId); + qDebug() << "AccountList: Pushed account with internal ID " << accountId << " to the front of the queue"; + if (!isActive()) + { + tryNext(); + } +} + +void AccountList::queueRefresh(QString accountId) +{ + if (m_refreshQueue.indexOf(accountId) != -1) + { + return; + } + m_refreshQueue.push_back(accountId); + qDebug() << "AccountList: Queued account with internal ID " << accountId << " to refresh"; +} + +void AccountList::tryNext() +{ + while (m_refreshQueue.length()) + { + auto accountId = m_refreshQueue.front(); + m_refreshQueue.pop_front(); + for (int i = 0; i < count(); i++) + { + auto account = at(i); + if (account->internalId() == accountId) + { + m_currentTask = account->refresh(); + if (m_currentTask) + { + connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed); + m_currentTask->start(); + qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() + << " with internal ID " << accountId; + return; + } + } + } + qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; + } + // if we get here, no account needed refreshing. Schedule refresh in an hour. + m_refreshTimer->start(1000 * 3600); +} + +void AccountList::authSucceeded() +{ + qDebug() << "RefreshSchedule: Background account refresh succeeded"; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +void AccountList::authFailed(QString reason) +{ + qDebug() << "RefreshSchedule: Background account refresh failed: " << reason; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +bool AccountList::isActive() const +{ + return m_activityCount != 0; +} + +void AccountList::beginActivity() +{ + bool activating = m_activityCount == 0; + m_activityCount++; + if (activating) + { + emit activityChanged(true); + } +} + +void AccountList::endActivity() +{ + if (m_activityCount == 0) + { + qWarning() << m_name << " - Activity count would become below zero"; + return; + } + bool deactivating = m_activityCount == 1; + m_activityCount--; + if (deactivating) + { + emit activityChanged(false); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountList.hpp b/archived/projt-launcher/launcher/minecraft/auth/AccountList.hpp new file mode 100644 index 0000000000..107c4bb286 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountList.hpp @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "MinecraftAccount.hpp" +#include "minecraft/auth/AuthFlow.hpp" + +#include +#include +#include +#include + +/*! + * List of available Mojang accounts. + * This should be loaded in the background by ProjT Launcher on startup. + */ +class AccountList : public QAbstractListModel +{ + Q_OBJECT + public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + IconColumn = 0, + ProfileNameColumn, + NameColumn, + TypeColumn, + StatusColumn, + + NUM_COLUMNS + }; + + explicit AccountList(QObject* parent = 0); + virtual ~AccountList() noexcept; + + const MinecraftAccountPtr at(int i) const; + int count() const; + + //////// List Model Functions //////// + QVariant data(const QModelIndex& index, int role) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex& parent) const override; + virtual int columnCount(const QModelIndex& parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override; + + void addAccount(MinecraftAccountPtr account); + void removeAccount(QModelIndex index); + int findAccountByProfileId(const QString& profileId) const; + MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const; + QStringList profileNames() const; + + // requesting a refresh pushes it to the front of the queue + void requestRefresh(QString accountId); + // queuing a refresh will let it go to the back of the queue (unless it's somewhere inside the queue already) + void queueRefresh(QString accountId); + + /*! + * Sets the path to load/save the list file from/to. + * If autosave is true, this list will automatically save to the given path whenever it changes. + * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately + * after calling this function to ensure an autosaved change doesn't overwrite the list you intended + * to load. + */ + void setListFilePath(QString path, bool autosave = false); + + bool loadList(); + bool loadV3(QJsonObject& root); + bool saveList(); + + MinecraftAccountPtr defaultAccount() const; + void setDefaultAccount(MinecraftAccountPtr profileId); + bool anyAccountIsValid(); + + bool isActive() const; + + protected: + void beginActivity(); + void endActivity(); + + private: + const char* m_name; + uint32_t m_activityCount = 0; + signals: + void listChanged(); + void listActivityChanged(); + void defaultAccountChanged(); + void activityChanged(bool active); + void fileSaveFailed(QString path); + + public slots: + /** + * This is called when one of the accounts changes and the list needs to be updated + */ + void accountChanged(); + + /** + * This is called when a (refresh/login) task involving the account starts or ends + */ + void accountActivityChanged(bool active); + + /** + * This is initially to run background account refresh tasks, or on a hourly timer + */ + void fillQueue(); + + private slots: + void tryNext(); + + void authSucceeded(); + void authFailed(QString reason); + + protected: + QList m_refreshQueue; + QTimer* m_refreshTimer; + QTimer* m_nextTimer; + shared_qobject_ptr m_currentTask; + + /*! + * Called whenever the list changes. + * This emits the listChanged() signal and autosaves the list (if autosave is enabled). + */ + void onListChanged(); + + /*! + * Called whenever the active account changes. + * Emits the defaultAccountChanged() signal and autosaves the list if enabled. + */ + void onDefaultAccountChanged(); + + QList m_accounts; + + MinecraftAccountPtr m_defaultAccount; + + //! Path to the account list file. Empty string if there isn't one. + QString m_listFilePath; + + /*! + * If true, the account list will automatically save to the account list path when it changes. + * Ignored if m_listFilePath is blank. + */ + bool m_autosave = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.cpp b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.cpp new file mode 100644 index 0000000000..7b20dc5a3f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.cpp @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "AuthFlow.hpp" + +#include + +#include "Application.h" +#include "minecraft/auth/steps/Steps.hpp" + +AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_legacyData(data) +{ + Q_ASSERT(data != nullptr); + + // Initialize credentials from legacy data if refreshing + if (action == Action::Refresh && data) + { + m_credentials.msaClientId = data->msaClientID; + m_credentials.msaToken.accessToken = data->msaToken.token; + m_credentials.msaToken.refreshToken = data->msaToken.refresh_token; + m_credentials.msaToken.issuedAt = data->msaToken.issueInstant; + m_credentials.msaToken.expiresAt = data->msaToken.notAfter; + m_credentials.msaToken.metadata = data->msaToken.extra; + } + + m_pipelineValid = buildPipeline(action); + + if (!m_pipelineValid) + { + qWarning() << "AuthFlow: Pipeline build failed for account type" << static_cast(data->type); + } + + updateState(AccountTaskState::STATE_CREATED); +} + +bool AuthFlow::buildPipeline(Action action) +{ + // Explicit handling of non-MSA accounts + if (m_legacyData->type == AccountType::Offline) + { + qDebug() << "AuthFlow: Offline account does not require authentication pipeline"; + // Offline accounts don't need auth steps - this is valid, not an error + // The caller should check account type before creating AuthFlow + return false; + } + + if (m_legacyData->type != AccountType::MSA) + { + qWarning() << "AuthFlow: Unsupported account type:" << static_cast(m_legacyData->type); + return false; + } + + // Step 1: Microsoft Authentication + if (action == Action::DeviceCode) + { + auto* deviceCodeStep = new projt::minecraft::auth::DeviceCodeAuthStep(m_credentials); + connect(deviceCodeStep, + &projt::minecraft::auth::DeviceCodeAuthStep::deviceCodeReady, + this, + &AuthFlow::authorizeWithBrowserWithExtra); + connect(this, &Task::aborted, deviceCodeStep, &projt::minecraft::auth::DeviceCodeAuthStep::cancel); + m_steps.append(projt::minecraft::auth::Step::Ptr(deviceCodeStep)); + } + else + { + auto* oauthStep = new projt::minecraft::auth::MicrosoftOAuthStep(m_credentials, action == Action::Refresh); + connect(oauthStep, + &projt::minecraft::auth::MicrosoftOAuthStep::browserAuthRequired, + this, + &AuthFlow::authorizeWithBrowser); + m_steps.append(projt::minecraft::auth::Step::Ptr(oauthStep)); + } + + // Step 2: Xbox Live User Token + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::XboxLiveUserStep(m_credentials))); + + // Step 3: Xbox XSTS Token for Xbox Live services + m_steps.append(projt::minecraft::auth::Step::Ptr( + new projt::minecraft::auth::XboxSecurityTokenStep(m_credentials, + projt::minecraft::auth::XstsTarget::XboxLive))); + + // Step 4: Xbox XSTS Token for Minecraft services + m_steps.append(projt::minecraft::auth::Step::Ptr( + new projt::minecraft::auth::XboxSecurityTokenStep(m_credentials, + projt::minecraft::auth::XstsTarget::MinecraftServices))); + + // Step 5: Minecraft Services Login (get access token) + m_steps.append( + projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::MinecraftServicesLoginStep(m_credentials))); + + // Step 6: Xbox Profile (optional, for display - gamertag extraction) + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::XboxProfileFetchStep(m_credentials))); + + // Step 7: Game Entitlements + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::GameEntitlementsStep(m_credentials))); + + // Step 8: Minecraft Profile + m_steps.append( + projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::MinecraftProfileFetchStep(m_credentials))); + + // Step 9: Skin Download + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::SkinDownloadStep(m_credentials))); + + qDebug() << "AuthFlow: Built pipeline with" << m_steps.size() << "steps"; + return true; +} + +void AuthFlow::executeTask() +{ + // Handle offline accounts - they don't need authentication + if (m_legacyData->type == AccountType::Offline) + { + qDebug() << "AuthFlow: Offline account - no authentication required, succeeding immediately"; + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Online; + } + updateState(AccountTaskState::STATE_SUCCEEDED, tr("Offline account ready")); + return; + } + + // Early fail for invalid pipeline (non-offline accounts) + if (!m_pipelineValid) + { + failWithState(AccountTaskState::STATE_FAILED_HARD, + tr("Failed to build authentication pipeline for this account type")); + return; + } + + // Sanity check: empty pipeline should not succeed silently + if (m_steps.isEmpty()) + { + qWarning() << "AuthFlow: Pipeline is empty after successful build - this is a bug"; + failWithState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication pipeline is empty (internal error)")); + return; + } + + updateState(AccountTaskState::STATE_WORKING, tr("Initializing")); + executeNextStep(); +} + +void AuthFlow::executeNextStep() +{ + // Check abort flag before starting new step + if (m_aborted) + { + qDebug() << "AuthFlow: Skipping next step - flow was aborted"; + return; + } + + if (!Task::isRunning()) + { + return; + } + + if (m_steps.isEmpty()) + { + m_currentStep.reset(); + succeed(); + return; + } + + m_currentStep = m_steps.front(); + m_steps.pop_front(); + + qDebug() << "AuthFlow:" << m_currentStep->description(); + + connect(m_currentStep.get(), &projt::minecraft::auth::Step::completed, this, &AuthFlow::onStepCompleted); + + m_currentStep->execute(); +} + +void AuthFlow::onStepCompleted(projt::minecraft::auth::StepResult result, QString message) +{ + // Map step result to flow state + // Note: StepResult::Succeeded means "step succeeded, continue flow" + // The flow itself only succeeds when all steps complete (pipeline empty) + const auto flowState = stepResultToFlowState(result); + + if (updateState(flowState, message)) + { + executeNextStep(); + } +} + +void AuthFlow::succeed() +{ + // Sync new credentials back to legacy AccountData + if (m_legacyData) + { + m_legacyData->msaClientID = m_credentials.msaClientId; + m_legacyData->msaToken.token = m_credentials.msaToken.accessToken; + m_legacyData->msaToken.refresh_token = m_credentials.msaToken.refreshToken; + m_legacyData->msaToken.issueInstant = m_credentials.msaToken.issuedAt; + m_legacyData->msaToken.notAfter = m_credentials.msaToken.expiresAt; + m_legacyData->msaToken.extra = m_credentials.msaToken.metadata; + m_legacyData->msaToken.validity = toValidity(m_credentials.msaToken.validity); + + m_legacyData->userToken.token = m_credentials.xboxUserToken.accessToken; + m_legacyData->userToken.issueInstant = m_credentials.xboxUserToken.issuedAt; + m_legacyData->userToken.notAfter = m_credentials.xboxUserToken.expiresAt; + m_legacyData->userToken.extra = m_credentials.xboxUserToken.metadata; + m_legacyData->userToken.validity = toValidity(m_credentials.xboxUserToken.validity); + + // xboxApiToken receives gamertag from XboxProfileFetchStep via xboxServiceToken.metadata + m_legacyData->xboxApiToken.token = m_credentials.xboxServiceToken.accessToken; + m_legacyData->xboxApiToken.issueInstant = m_credentials.xboxServiceToken.issuedAt; + m_legacyData->xboxApiToken.notAfter = m_credentials.xboxServiceToken.expiresAt; + m_legacyData->xboxApiToken.extra = m_credentials.xboxServiceToken.metadata; + m_legacyData->xboxApiToken.validity = toValidity(m_credentials.xboxServiceToken.validity); + + m_legacyData->mojangservicesToken.token = m_credentials.minecraftServicesToken.accessToken; + m_legacyData->mojangservicesToken.issueInstant = m_credentials.minecraftServicesToken.issuedAt; + m_legacyData->mojangservicesToken.notAfter = m_credentials.minecraftServicesToken.expiresAt; + m_legacyData->mojangservicesToken.extra = m_credentials.minecraftServicesToken.metadata; + m_legacyData->mojangservicesToken.validity = toValidity(m_credentials.minecraftServicesToken.validity); + + m_legacyData->yggdrasilToken.token = m_credentials.minecraftAccessToken.accessToken; + m_legacyData->yggdrasilToken.issueInstant = m_credentials.minecraftAccessToken.issuedAt; + m_legacyData->yggdrasilToken.notAfter = m_credentials.minecraftAccessToken.expiresAt; + m_legacyData->yggdrasilToken.validity = toValidity(m_credentials.minecraftAccessToken.validity); + + m_legacyData->minecraftProfile.id = m_credentials.profile.id; + m_legacyData->minecraftProfile.name = m_credentials.profile.name; + m_legacyData->minecraftProfile.skin.id = m_credentials.profile.skin.id; + m_legacyData->minecraftProfile.skin.url = m_credentials.profile.skin.url; + m_legacyData->minecraftProfile.skin.variant = m_credentials.profile.skin.variant; + m_legacyData->minecraftProfile.skin.data = m_credentials.profile.skin.imageData; + m_legacyData->minecraftProfile.validity = toValidity(m_credentials.profile.validity); + m_legacyData->minecraftProfile.currentCape = m_credentials.profile.activeCapeId; + + // Sync capes + m_legacyData->minecraftProfile.capes.clear(); + for (auto it = m_credentials.profile.capes.begin(); it != m_credentials.profile.capes.end(); ++it) + { + const auto& capeIn = it.value(); + Cape capeOut; + capeOut.id = capeIn.id; + capeOut.url = capeIn.url; + capeOut.alias = capeIn.alias; + capeOut.data = capeIn.imageData; + m_legacyData->minecraftProfile.capes.insert(capeIn.id, capeOut); + } + + m_legacyData->minecraftEntitlement.ownsMinecraft = m_credentials.entitlements.ownsMinecraft; + m_legacyData->minecraftEntitlement.canPlayMinecraft = m_credentials.entitlements.canPlayMinecraft; + m_legacyData->minecraftEntitlement.validity = toValidity(m_credentials.entitlements.validity); + + m_legacyData->validity_ = Validity::Certain; + } + + updateState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps")); +} + +void AuthFlow::failWithState(AccountTaskState state, const QString& reason) +{ + if (m_legacyData) + { + m_legacyData->errorString = reason; + } + updateState(state, reason); +} + +bool AuthFlow::updateState(AccountTaskState newState, const QString& reason) +{ + m_taskState = newState; + setDetails(reason); + + switch (newState) + { + case AccountTaskState::STATE_CREATED: + setStatus(tr("Waiting...")); + if (m_legacyData) + { + m_legacyData->errorString.clear(); + } + return true; + + case AccountTaskState::STATE_WORKING: + setStatus(m_currentStep ? m_currentStep->description() : tr("Working...")); + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Working; + } + return true; + + case AccountTaskState::STATE_SUCCEEDED: + setStatus(tr("Authentication task succeeded.")); + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Online; + } + emitSucceeded(); + return false; + + case AccountTaskState::STATE_OFFLINE: + setStatus(tr("Failed to contact the authentication server.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Offline; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_DISABLED: + setStatus(tr("Client ID has changed. New session needs to be created.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Disabled; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_FAILED_SOFT: + setStatus(tr("Encountered an error during authentication.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Errored; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_FAILED_HARD: + setStatus(tr("Failed to authenticate. The session has expired.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Expired; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_FAILED_GONE: + setStatus(tr("Failed to authenticate. The account no longer exists.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Gone; + } + emitFailed(reason); + return false; + + default: + setStatus(tr("...")); + const QString error = tr("Unknown account task state: %1").arg(static_cast(newState)); + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Errored; + } + emitFailed(error); + return false; + } +} + +AccountTaskState AuthFlow::stepResultToFlowState(projt::minecraft::auth::StepResult result) noexcept +{ + // StepResult::Continue and StepResult::Succeeded both mean "step completed successfully" + // The distinction is semantic: Continue hints more steps may follow, Succeeded suggests finality. + // At the flow level, both translate to STATE_WORKING until the pipeline is exhausted. + // + // Future: If we add optional/best-effort steps, we may want a StepResult::Skipped that + // also maps to STATE_WORKING but logs differently. + + switch (result) + { + case projt::minecraft::auth::StepResult::Continue: + case projt::minecraft::auth::StepResult::Succeeded: return AccountTaskState::STATE_WORKING; + + case projt::minecraft::auth::StepResult::Offline: return AccountTaskState::STATE_OFFLINE; + + case projt::minecraft::auth::StepResult::SoftFailure: return AccountTaskState::STATE_FAILED_SOFT; + + case projt::minecraft::auth::StepResult::HardFailure: return AccountTaskState::STATE_FAILED_HARD; + + case projt::minecraft::auth::StepResult::Disabled: return AccountTaskState::STATE_DISABLED; + + case projt::minecraft::auth::StepResult::Gone: return AccountTaskState::STATE_FAILED_GONE; + } + + return AccountTaskState::STATE_FAILED_HARD; +} + +Validity AuthFlow::toValidity(projt::minecraft::auth::TokenValidity validity) noexcept +{ + switch (validity) + { + case projt::minecraft::auth::TokenValidity::None: return Validity::None; + case projt::minecraft::auth::TokenValidity::Assumed: return Validity::Assumed; + case projt::minecraft::auth::TokenValidity::Certain: return Validity::Certain; + } + return Validity::None; +} + +bool AuthFlow::abort() +{ + // Set abort flag to prevent new steps from starting + m_aborted = true; + + qDebug() << "AuthFlow: Abort requested"; + + // Cancel current step BEFORE emitting aborted (to prevent use-after-free) + // The emitAborted() signal may cause this object to be destroyed + if (m_currentStep) + { + // Disconnect to prevent callbacks after abort + disconnect(m_currentStep.get(), nullptr, this, nullptr); + m_currentStep->cancel(); + m_currentStep.reset(); + } + + // Clear remaining steps + m_steps.clear(); + + emitAborted(); + + return true; +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.hpp b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.hpp new file mode 100644 index 0000000000..86fac44286 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.hpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "minecraft/auth/AccountData.hpp" +#include "minecraft/auth/steps/Step.hpp" +#include "minecraft/auth/steps/Credentials.hpp" +#include "tasks/Task.h" + +class AuthFlow : public Task +{ + Q_OBJECT + + public: + /** + * Authentication action to perform. + */ + enum class Action + { + Refresh, ///< Silent token refresh + Login, ///< Interactive browser login + DeviceCode ///< Device code flow + }; + + explicit AuthFlow(AccountData* data, Action action = Action::Refresh); + ~AuthFlow() override = default; + + void executeTask() override; + + [[nodiscard]] AccountTaskState taskState() const noexcept + { + return m_taskState; + } + + public slots: + bool abort() override; + + signals: + /** + * Emitted when browser authorization is required. + */ + void authorizeWithBrowser(const QUrl& url); + + /** + * Emitted when device code authorization is required. + */ + void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); + + private slots: + void onStepCompleted(projt::minecraft::auth::StepResult result, QString message); + + private: + /** + * Build the authentication pipeline based on account type and action. + * @return true if pipeline was successfully built, false on configuration error + */ + [[nodiscard]] bool buildPipeline(Action action); + + void executeNextStep(); + void succeed(); + void failWithState(AccountTaskState state, const QString& reason); + bool updateState(AccountTaskState newState, const QString& reason = QString()); + + // Convert new StepResult to intermediate flow state (not final AccountTaskState) + [[nodiscard]] static AccountTaskState stepResultToFlowState(projt::minecraft::auth::StepResult result) noexcept; + + // Convert new TokenValidity to legacy Validity + [[nodiscard]] static Validity toValidity(projt::minecraft::auth::TokenValidity validity) noexcept; + + AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; + QList m_steps; + projt::minecraft::auth::Step::Ptr m_currentStep; + + // Flow control + bool m_aborted = false; + bool m_pipelineValid = false; + + // Legacy AccountData for compatibility with existing consumers + AccountData* m_legacyData = nullptr; + + // New credentials structure (populated during auth, synced to legacy at end) + projt::minecraft::auth::Credentials m_credentials; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthSession.cpp b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.cpp new file mode 100644 index 0000000000..cedb2e7579 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "AuthSession.hpp" +#include +#include +#include +#include + +QString AuthSession::serializeUserProperties() +{ + QJsonObject userAttrs; + /* + for (auto key : u.properties.keys()) + { + auto array = QJsonArray::fromStringList(u.properties.values(key)); + userAttrs.insert(key, array); + } + */ + QJsonDocument value(userAttrs); + return value.toJson(QJsonDocument::Compact); +} + +bool AuthSession::MakeOffline(QString offline_playername) +{ + session = "-"; + access_token = "0"; + player_name = offline_playername; + launchMode = LaunchMode::Offline; + wants_online = false; + demo = false; + status = PlayableOffline; + return true; +} + +void AuthSession::MakeDemo(QString name, QString u) +{ + launchMode = LaunchMode::Demo; + wants_online = false; + demo = true; + uuid = u; + session = "-"; + access_token = "0"; + player_name = name; + status = PlayableOnline; // needs online to download the assets +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthSession.hpp b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.hpp new file mode 100644 index 0000000000..10092a0e91 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.hpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +#include "LaunchMode.h" + +class MinecraftAccount; + +struct AuthSession +{ + bool MakeOffline(QString offline_playername); + void MakeDemo(QString name, QString uuid); + + QString serializeUserProperties(); + + enum Status + { + Undetermined, + RequiresOAuth, + RequiresPassword, + RequiresProfileSetup, + PlayableOffline, + PlayableOnline, + GoneOrMigrated + } status = Undetermined; + + // combined session ID + QString session; + // volatile auth token + QString access_token; + // profile name + QString player_name; + // profile ID + QString uuid; + // 'legacy' or 'mojang', depending on account type + QString user_type; + // The resolved launch mode for this session. + LaunchMode launchMode = LaunchMode::Normal; + // Did the auth server reply? + bool auth_server_online = false; + // Did the user request online mode? + bool wants_online = true; + + // Is this a demo session? + bool demo = false; +}; + +using AuthSessionPtr = std::shared_ptr; diff --git a/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.cpp b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.cpp new file mode 100644 index 0000000000..5435b62809 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.cpp @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "MinecraftAccount.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "minecraft/auth/AccountData.hpp" +#include "minecraft/auth/AuthFlow.hpp" + +MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) +{ + data.internalId = QUuid::createUuid().toString(QUuid::Id128); +} + +MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) +{ + MinecraftAccountPtr account(new MinecraftAccount()); + if (account->data.resumeStateFromV3(json)) + { + return account; + } + return nullptr; +} + +MinecraftAccountPtr MinecraftAccount::createBlankMSA() +{ + MinecraftAccountPtr account(new MinecraftAccount()); + account->data.type = AccountType::MSA; + return account; +} + +MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) +{ + auto account = makeShared(); + account->data.type = AccountType::Offline; + account->data.yggdrasilToken.token = "0"; + account->data.yggdrasilToken.validity = Validity::Certain; + account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128); + account->data.minecraftProfile.id = uuidFromUsername(username).toString(QUuid::Id128); + account->data.minecraftProfile.name = username; + account->data.minecraftProfile.validity = Validity::Certain; + return account; +} + +QJsonObject MinecraftAccount::saveToJson() const +{ + return data.saveState(); +} + +AccountState MinecraftAccount::accountState() const +{ + return data.accountState; +} + +QPixmap MinecraftAccount::getFace() const +{ + QPixmap skinTexture; + if (!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) + { + return QPixmap(); + } + QPixmap skin = QPixmap(8, 8); + skin.fill(QColorConstants::Transparent); + QPainter painter(&skin); + painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); + painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); + return skin.scaled(64, 64, Qt::KeepAspectRatio); +} + +shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) +{ + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login)); + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); + connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::refresh() +{ + if (m_currentTask) + { + return m_currentTask; + } + + m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh)); + + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); + connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::currentTask() +{ + return m_currentTask; +} + +void MinecraftAccount::authSucceeded() +{ + m_currentTask.reset(); + emit authenticationSucceeded(); + emit changed(); + emit activityChanged(false); +} + +void MinecraftAccount::authFailed(QString reason) +{ + auto taskState = m_currentTask->taskState(); + emit authenticationFailed(reason, taskState); + + switch (taskState) + { + case AccountTaskState::STATE_OFFLINE: + case AccountTaskState::STATE_DISABLED: + { + // NOTE: user will need to fix this themselves. + } + case AccountTaskState::STATE_FAILED_SOFT: + { + // NOTE: this doesn't do much. There was an error of some sort. + } + break; + case AccountTaskState::STATE_FAILED_HARD: + { + if (accountType() == AccountType::MSA) + { + data.msaToken.token = QString(); + data.msaToken.refresh_token = QString(); + data.msaToken.validity = Validity::None; + data.validity_ = Validity::None; + } + else + { + data.yggdrasilToken.token = QString(); + data.yggdrasilToken.validity = Validity::None; + data.validity_ = Validity::None; + } + emit validityChanged(Validity::None); + emit changed(); + } + break; + case AccountTaskState::STATE_FAILED_GONE: + { + data.validity_ = Validity::None; + emit validityChanged(Validity::None); + emit changed(); + } + break; + case AccountTaskState::STATE_WORKING: + { + data.accountState = AccountState::Unchecked; + emit accountStateChanged(AccountState::Unchecked); + } + break; + case AccountTaskState::STATE_CREATED: + case AccountTaskState::STATE_SUCCEEDED: + { + // Not reachable here, as they are not failures. + } + } + m_currentTask.reset(); + emit activityChanged(false); + emit authenticationError(reason); +} + +QString MinecraftAccount::displayName() const +{ + const QList validStates{ AccountState::Unchecked, AccountState::Working, AccountState::Offline, AccountState::Online }; + if (!validStates.contains(accountState())) + { + return QString("⚠ %1").arg(profileName()); + } + return profileName(); +} + +bool MinecraftAccount::isActive() const +{ + return !m_currentTask.isNull(); +} + +bool MinecraftAccount::shouldRefresh() const +{ + /* + * Never refresh accounts that are being used by the game, it breaks the game session. + * Always refresh accounts that have not been refreshed yet during this session. + * Don't refresh broken accounts. + * Refresh accounts that would expire in the next 12 hours (fresh token validity is 24 hours). + */ + if (isInUse()) + { + return false; + } + switch (data.validity_) + { + case Validity::Certain: + { + break; + } + case Validity::None: + { + return false; + } + case Validity::Assumed: + { + return true; + } + } + auto now = QDateTime::currentDateTimeUtc(); + auto issuedTimestamp = data.yggdrasilToken.issueInstant; + auto expiresTimestamp = data.yggdrasilToken.notAfter; + + if (!expiresTimestamp.isValid()) + { + expiresTimestamp = issuedTimestamp.addSecs(24 * 3600); + } + if (now.secsTo(expiresTimestamp) < (12 * 3600)) + { + return true; + } + return false; +} + +void MinecraftAccount::fillSession(AuthSessionPtr session) +{ + session->wants_online = session->launchMode != LaunchMode::Offline; + session->demo = session->launchMode == LaunchMode::Demo; + + if (ownsMinecraft() && !hasProfile()) + { + session->status = AuthSession::RequiresProfileSetup; + } + else + { + if (session->launchMode == LaunchMode::Offline) + { + session->status = AuthSession::PlayableOffline; + } + else + { + session->status = AuthSession::PlayableOnline; + } + } + + // volatile auth token + session->access_token = data.accessToken(); + // profile name + session->player_name = data.profileName(); + // profile ID + session->uuid = data.profileId(); + if (session->uuid.isEmpty()) + session->uuid = uuidFromUsername(session->player_name).toString(QUuid::Id128); + // 'legacy' or 'mojang', depending on account type + session->user_type = typeString(); + if (!session->access_token.isEmpty()) + { + session->session = "token:" + data.accessToken() + ":" + data.profileId(); + } + else + { + session->session = "-"; + } +} + +void MinecraftAccount::decrementUses() +{ + Usable::decrementUses(); + if (!isInUse()) + { + emit changed(); + // Using internalId for account identification (profile may not be set for new accounts) + qWarning() << "Account" << data.internalId << "(" << data.profileName() << ") is no longer in use."; + } +} + +void MinecraftAccount::incrementUses() +{ + bool wasInUse = isInUse(); + Usable::incrementUses(); + if (!wasInUse) + { + emit changed(); + // Using internalId for account identification (profile may not be set for new accounts) + qWarning() << "Account" << data.internalId << "(" << data.profileName() << ") is now in use."; + } +} + +QUuid MinecraftAccount::uuidFromUsername(QString username) +{ + auto input = QString("OfflinePlayer:%1").arg(username).toUtf8(); + + // basically a reimplementation of Java's UUID#nameUUIDFromBytes + QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); + + auto bOr = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] |= value; }; + auto bAnd = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] &= value; }; + bAnd(digest, 6, 0x0f); // clear version + bOr(digest, 6, 0x30); // set to version 3 + bAnd(digest, 8, 0x3f); // clear variant + bOr(digest, 8, 0x80); // set to IETF variant + + return QUuid::fromRfc4122(digest); +} diff --git a/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.hpp b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.hpp new file mode 100644 index 0000000000..8af81e0a26 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.hpp @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "AccountData.hpp" +#include "AuthSession.hpp" +#include "QObjectPtr.h" +#include "Usable.h" +#include "minecraft/auth/AuthFlow.hpp" + +class Task; +class MinecraftAccount; + +using MinecraftAccountPtr = shared_qobject_ptr; +Q_DECLARE_METATYPE(MinecraftAccountPtr) + +/** + * A profile within someone's Mojang account. + * + * Currently, the profile system has not been implemented by Mojang yet, + * but we might as well add some things for it in ProjT Launcher right now so + * we don't have to rip the code to pieces to add it later. + */ +struct AccountProfile +{ + QString id; + QString name; + bool legacy; +}; + +/** + * Object that stores information about a certain Mojang account. + * + * Said information may include things such as that account's username, client token, and access + * token if the user chose to stay logged in. + */ +class MinecraftAccount : public QObject, public Usable +{ + Q_OBJECT + public: /* construction */ + //! Do not copy accounts. ever. + explicit MinecraftAccount(const MinecraftAccount& other, QObject* parent) = delete; + + //! Default constructor + explicit MinecraftAccount(QObject* parent = 0); + + static MinecraftAccountPtr createBlankMSA(); + + static MinecraftAccountPtr createOffline(const QString& username); + + static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json); + + static QUuid uuidFromUsername(QString username); + + //! Saves a MinecraftAccount to a JSON object and returns it. + QJsonObject saveToJson() const; + + public: /* manipulation */ + shared_qobject_ptr login(bool useDeviceCode = false); + + shared_qobject_ptr refresh(); + + shared_qobject_ptr currentTask(); + + public: /* queries */ + QString internalId() const + { + return data.internalId; + } + + QString accountDisplayString() const + { + return data.accountDisplayString(); + } + + QString accessToken() const + { + return data.accessToken(); + } + + QString profileId() const + { + return data.profileId(); + } + + QString profileName() const + { + return data.profileName(); + } + + QString displayName() const; + + bool isActive() const; + + AccountType accountType() const noexcept + { + return data.type; + } + + bool ownsMinecraft() const + { + return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; + } + + bool hasProfile() const + { + return data.profileId().size() != 0; + } + + QString typeString() const + { + switch (data.type) + { + case AccountType::MSA: + { + return "msa"; + } + break; + case AccountType::Offline: + { + return "offline"; + } + break; + default: + { + return "unknown"; + } + } + } + + QPixmap getFace() const; + + //! Returns the current state of the account + AccountState accountState() const; + + AccountData* accountData() + { + return &data; + } + + bool shouldRefresh() const; + + void fillSession(AuthSessionPtr session); + + QString lastError() const + { + return data.lastError(); + } + + signals: + /** + * This signal is emitted when the account changes + */ + void changed(); + + void activityChanged(bool active); + + // Specific signals for different state changes + void accountStateChanged(AccountState newState); + void authenticationSucceeded(); + void authenticationFailed(QString reason, AccountTaskState taskState); + void profileUpdated(); + void validityChanged(Validity newValidity); + /// Emitted when an authentication error occurs + void authenticationError(QString errorMessage); + + protected: /* variables */ + AccountData data; + + // current task we are executing here + shared_qobject_ptr m_currentTask; + + protected: /* methods */ + void incrementUses() override; + void decrementUses() override; + + private slots: + void authSucceeded(); + void authFailed(QString reason); +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/Parsers.cpp b/archived/projt-launcher/launcher/minecraft/auth/Parsers.cpp new file mode 100644 index 0000000000..184de65898 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/Parsers.cpp @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "Parsers.hpp" +#include "Json.h" +#include "minecraft/Logging.h" + +#include +#include +#include + +namespace Parsers +{ + + bool getDateTime(QJsonValue value, QDateTime& out) + { + if (!value.isString()) + { + return false; + } + out = QDateTime::fromString(value.toString(), Qt::ISODate); + return out.isValid(); + } + + bool getString(QJsonValue value, QString& out) + { + if (!value.isString()) + { + return false; + } + out = value.toString(); + return true; + } + + bool getNumber(QJsonValue value, double& out) + { + if (!value.isDouble()) + { + return false; + } + out = value.toDouble(); + return true; + } + + bool getNumber(QJsonValue value, int64_t& out) + { + if (!value.isDouble()) + { + return false; + } + out = (int64_t)value.toDouble(); + return true; + } + + bool getBool(QJsonValue value, bool& out) + { + if (!value.isBool()) + { + return false; + } + out = value.toBool(); + return true; + } + + /* + { + "IssueInstant":"2020-12-07T19:52:08.4463796Z", + "NotAfter":"2020-12-21T19:52:08.4463796Z", + "Token":"token", + "DisplayClaims":{ + "xui":[ + { + "uhs":"userhash" + } + ] + } + } + */ + // Error responses from Xbox Live are handled in parseXTokenResponse below. + // Known error codes: + // - 2148916233: Missing Xbox account + // - 2148916238: Child account not linked to a family + /* + { + "Identity":"0", + "XErr":2148916238, + "Message":"", + "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" + } + */ + + bool parseXTokenResponse(QByteArray& data, Token& output, QString name) + { + qDebug() << "Parsing" << name << ":"; + qCDebug(authCredentials()) << data; + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if (!getDateTime(obj.value("IssueInstant"), output.issueInstant)) + { + qWarning() << "User IssueInstant is not a timestamp"; + return false; + } + if (!getDateTime(obj.value("NotAfter"), output.notAfter)) + { + qWarning() << "User NotAfter is not a timestamp"; + return false; + } + if (!getString(obj.value("Token"), output.token)) + { + qWarning() << "User Token is not a string"; + return false; + } + auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); + if (!arrayVal.isArray()) + { + qWarning() << "Missing xui claims array"; + return false; + } + bool foundUHS = false; + for (auto item : arrayVal.toArray()) + { + if (!item.isObject()) + { + continue; + } + auto obj_ = item.toObject(); + if (obj_.contains("uhs")) + { + foundUHS = true; + } + else + { + continue; + } + // consume all 'display claims' ... whatever that means + for (auto iter = obj_.begin(); iter != obj_.end(); iter++) + { + QString claim; + if (!getString(obj_.value(iter.key()), claim)) + { + qWarning() << "display claim " << iter.key() << " is not a string..."; + return false; + } + output.extra[iter.key()] = claim; + } + + break; + } + if (!foundUHS) + { + qWarning() << "Missing uhs"; + return false; + } + output.validity = Validity::Certain; + qDebug() << name << "is valid."; + return true; + } + + bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) + { + qDebug() << "Parsing Minecraft profile..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if (!getString(obj.value("id"), output.id)) + { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if (!getString(obj.value("name"), output.name)) + { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto skinsArray = obj.value("skins").toArray(); + for (auto skin : skinsArray) + { + auto skinObj = skin.toObject(); + Skin skinOut; + if (!getString(skinObj.value("id"), skinOut.id)) + { + continue; + } + QString state; + if (!getString(skinObj.value("state"), state)) + { + continue; + } + if (state != "ACTIVE") + { + continue; + } + if (!getString(skinObj.value("url"), skinOut.url)) + { + continue; + } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + if (!getString(skinObj.value("variant"), skinOut.variant)) + { + continue; + } + // we deal with only the active skin + output.skin = skinOut; + break; + } + auto capesArray = obj.value("capes").toArray(); + + QString currentCape; + for (auto cape : capesArray) + { + auto capeObj = cape.toObject(); + Cape capeOut; + if (!getString(capeObj.value("id"), capeOut.id)) + { + continue; + } + QString state; + if (!getString(capeObj.value("state"), state)) + { + continue; + } + if (state == "ACTIVE") + { + currentCape = capeOut.id; + } + if (!getString(capeObj.value("url"), capeOut.url)) + { + continue; + } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + if (!getString(capeObj.value("alias"), capeOut.alias)) + { + continue; + } + + output.capes[capeOut.id] = capeOut; + } + output.currentCape = currentCape; + output.validity = Validity::Certain; + return true; + } + + namespace + { + // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) + // they are needed because the session server doesn't return skin urls for default skins + static const QString SKIN_URL_STEVE = + "https://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + static const QString SKIN_URL_ALEX = + "https://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + + bool isDefaultModelSteve(QString uuid) + { + // need to calculate *Java* hashCode of UUID + // if number is even, skin/model is steve, otherwise it is alex + + // just in case dashes are in the id + uuid.remove('-'); + + if (uuid.size() != 32) + { + return true; + } + + // qulonglong is guaranteed to be 64 bits + // we need to use unsigned numbers to guarantee truncation below + qulonglong most = uuid.left(16).toULongLong(nullptr, 16); + qulonglong least = uuid.right(16).toULongLong(nullptr, 16); + qulonglong xored = most ^ least; + return ((static_cast(xored >> 32)) ^ static_cast(xored)) % 2 == 0; + } + } // namespace + + /** + Uses session server for skin/cape lookup instead of profile, + because locked Mojang accounts cannot access profile endpoint + (https://api.minecraftservices.com/minecraft/profile/) + + ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape + + { + "id": "", + "name": "", + "properties": [ + { + "name": "textures", + "value": "" + } + ] + } + + decoded base64 "value": + { + "timestamp": , + "profileId": "", + "profileName": "", + "textures": { + "SKIN": { + "url": "" + }, + "CAPE": { + "url": "" + } + } + } + */ + + bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) + { + qDebug() << "Parsing Minecraft profile..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = Json::requireObject(doc, "mojang minecraft profile"); + if (!getString(obj.value("id"), output.id)) + { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if (!getString(obj.value("name"), output.name)) + { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto propsArray = obj.value("properties").toArray(); + QByteArray texturePayload; + for (auto p : propsArray) + { + auto pObj = p.toObject(); + auto name = pObj.value("name"); + if (!name.isString() || name.toString() != "textures") + { + continue; + } + + auto value = pObj.value("value"); + if (value.isString()) + { + texturePayload = + QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); + } + + if (!texturePayload.isEmpty()) + { + break; + } + } + + if (texturePayload.isNull()) + { + qWarning() << "No texture payload data"; + return false; + } + + doc = QJsonDocument::fromJson(texturePayload, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + obj = Json::requireObject(doc, "session texture payload"); + auto textures = obj.value("textures"); + if (!textures.isObject()) + { + qWarning() << "No textures array in response"; + return false; + } + + Skin skinOut; + // fill in default skin info ourselves, as this endpoint doesn't provide it + bool steve = isDefaultModelSteve(output.id); + skinOut.variant = steve ? "CLASSIC" : "SLIM"; + skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; + // sadly we can't figure this out, but I don't think it really matters... + skinOut.id = "00000000-0000-0000-0000-000000000000"; + Cape capeOut; + auto tObj = textures.toObject(); + for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) + { + if (idx->isObject()) + { + if (idx.key() == "SKIN") + { + auto skin = idx->toObject(); + if (!getString(skin.value("url"), skinOut.url)) + { + qWarning() << "Skin url is not a string"; + return false; + } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + + auto maybeMeta = skin.find("metadata"); + if (maybeMeta != skin.end() && maybeMeta->isObject()) + { + auto meta = maybeMeta->toObject(); + // might not be present + getString(meta.value("model"), skinOut.variant); + } + } + else if (idx.key() == "CAPE") + { + auto cape = idx->toObject(); + if (!getString(cape.value("url"), capeOut.url)) + { + qWarning() << "Cape url is not a string"; + return false; + } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + + // we don't know the cape ID as it is not returned from the session server + // so just fake it - changing capes is probably locked anyway :( + capeOut.alias = "cape"; + } + } + } + + output.skin = skinOut; + if (capeOut.alias == "cape") + { + output.capes = QMap({ { capeOut.alias, capeOut } }); + output.currentCape = capeOut.alias; + } + + output.validity = Validity::Certain; + return true; + } + + bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output) + { + qDebug() << "Parsing Minecraft entitlements..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + output.canPlayMinecraft = false; + output.ownsMinecraft = false; + + auto itemsArray = obj.value("items").toArray(); + for (auto item : itemsArray) + { + auto itemObj = item.toObject(); + QString name; + if (!getString(itemObj.value("name"), name)) + { + continue; + } + if (name == "game_minecraft") + { + output.canPlayMinecraft = true; + } + if (name == "product_minecraft") + { + output.ownsMinecraft = true; + } + } + output.validity = Validity::Certain; + return true; + } + + bool parseRolloutResponse(QByteArray& data, bool& result) + { + qDebug() << "Parsing Rollout response..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() + << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " + << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if (!getString(obj.value("feature"), feature)) + { + qWarning() << "Rollout feature is not a string"; + return false; + } + if (feature != "msamigration") + { + qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature + << "\""; + return false; + } + if (!getBool(obj.value("rollout"), result)) + { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; + } + + bool parseMojangResponse(QByteArray& data, Token& output) + { + QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; + qCDebug(authCredentials()) << data; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " + << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + double expires_in = 0; + if (!getNumber(obj.value("expires_in"), expires_in)) + { + qWarning() << "expires_in is not a valid number"; + return false; + } + auto currentTime = QDateTime::currentDateTimeUtc(); + output.issueInstant = currentTime; + output.notAfter = currentTime.addSecs(expires_in); + + QString username; + if (!getString(obj.value("username"), username)) + { + qWarning() << "username is not valid"; + return false; + } + + // Basic JWT structure validation: JWTs have 3 dot-separated parts (header.payload.signature) + QString accessToken; + if (!getString(obj.value("access_token"), accessToken)) + { + qWarning() << "access_token is not valid"; + return false; + } + auto parts = accessToken.split('.'); + if (parts.size() != 3) + { + qWarning() << "access_token is not a valid JWT (expected 3 parts, got" << parts.size() << ")"; + return false; + } + output.token = accessToken; + output.validity = Validity::Certain; + qDebug() << "Mojang response is valid."; + return true; + } + +} // namespace Parsers diff --git a/archived/projt-launcher/launcher/minecraft/auth/Parsers.hpp b/archived/projt-launcher/launcher/minecraft/auth/Parsers.hpp new file mode 100644 index 0000000000..577059e636 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/Parsers.hpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "AccountData.hpp" + +namespace Parsers +{ + bool getDateTime(QJsonValue value, QDateTime& out); + bool getString(QJsonValue value, QString& out); + bool getNumber(QJsonValue value, double& out); + bool getNumber(QJsonValue value, int64_t& out); + bool getBool(QJsonValue value, bool& out); + + bool parseXTokenResponse(QByteArray& data, Token& output, QString name); + bool parseMojangResponse(QByteArray& data, Token& output); + + bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output); + bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output); + bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output); + bool parseRolloutResponse(QByteArray& data, bool& result); +} // namespace Parsers diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/Credentials.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/Credentials.hpp new file mode 100644 index 0000000000..1de4898a67 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/Credentials.hpp @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace projt::minecraft::auth +{ + + /** + * Validity state for tokens and data. + */ + enum class TokenValidity + { + None, ///< Not validated or expired + Assumed, ///< Assumed valid (e.g., loaded from disk) + Certain ///< Verified valid by server + }; + + /** + * Generic OAuth/authentication token. + */ + struct AuthToken + { + QDateTime issuedAt; ///< When the token was issued + QDateTime expiresAt; ///< When the token expires + QString accessToken; ///< The access token value + QString refreshToken; ///< OAuth refresh token (if applicable) + QVariantMap metadata; ///< Additional token data (e.g., user hash) + + TokenValidity validity = TokenValidity::None; + bool persist = true; ///< Whether to save this token to disk + + [[nodiscard]] bool isExpired() const noexcept + { + return expiresAt.isValid() && QDateTime::currentDateTimeUtc() >= expiresAt; + } + + [[nodiscard]] bool hasRefreshToken() const noexcept + { + return !refreshToken.isEmpty(); + } + }; + + /** + * Minecraft player skin data. + */ + struct PlayerSkin + { + QString id; + QString url; + QString variant; ///< "CLASSIC" or "SLIM" + QByteArray imageData; + + [[nodiscard]] bool isEmpty() const noexcept + { + return id.isEmpty(); + } + }; + + /** + * Minecraft player cape data. + */ + struct PlayerCape + { + QString id; + QString url; + QString alias; + QByteArray imageData; + + [[nodiscard]] bool isEmpty() const noexcept + { + return id.isEmpty(); + } + }; + + /** + * Minecraft game entitlements (ownership info). + */ + struct GameEntitlements + { + bool ownsMinecraft = false; + bool canPlayMinecraft = false; + TokenValidity validity = TokenValidity::None; + + [[nodiscard]] bool isValid() const noexcept + { + return validity != TokenValidity::None; + } + }; + + /** + * Minecraft Java Edition profile. + */ + struct MinecraftJavaProfile + { + QString id; ///< UUID without dashes + QString name; ///< Player name (gamertag) + PlayerSkin skin; + QString activeCapeId; + QMap capes; + TokenValidity validity = TokenValidity::None; + + [[nodiscard]] bool hasProfile() const noexcept + { + return !id.isEmpty(); + } + [[nodiscard]] bool hasName() const noexcept + { + return !name.isEmpty(); + } + }; + + /** + * Account type enumeration. + */ + enum class AccountKind + { + Microsoft, ///< Microsoft/Xbox Live authenticated + Offline ///< Offline mode (no authentication) + }; + + /** + * Account status enumeration. + */ + enum class AccountStatus + { + Unchecked, ///< Not yet validated + Offline, ///< Network unavailable + Working, ///< Auth in progress + Online, ///< Fully authenticated + Disabled, ///< Disabled (e.g., client ID mismatch) + Error, ///< Error state + Expired, ///< Tokens expired, needs refresh + Gone ///< Account no longer exists + }; + + /** + * Complete authentication credentials for a Minecraft account. + * + * This structure holds all tokens and profile information needed to + * authenticate and play Minecraft. It is passed by reference to Step + * implementations which populate fields as authentication progresses. + */ + struct Credentials + { + // === Account identification === + AccountKind kind = AccountKind::Microsoft; + QString internalId; ///< Internal account identifier + QString msaClientId; ///< Microsoft Application client ID used + + // === Microsoft authentication chain === + AuthToken msaToken; ///< Microsoft OAuth token + AuthToken xboxUserToken; ///< XBL user token + AuthToken xboxServiceToken; ///< XSTS token for Xbox services + AuthToken minecraftServicesToken; ///< XSTS token for Minecraft services + + // === Minecraft authentication === + AuthToken minecraftAccessToken; ///< Yggdrasil-style access token + MinecraftJavaProfile profile; ///< Player profile + GameEntitlements entitlements; ///< Game ownership + + // === Runtime state (not persisted) === + AccountStatus status = AccountStatus::Unchecked; + QString lastError; + + // === Convenience accessors === + + /** + * Display string for this account (gamertag or profile name). + */ + [[nodiscard]] QString displayName() const noexcept + { + return profile.hasName() ? profile.name : QStringLiteral("(unknown)"); + } + + /** + * Access token to pass to the game. + */ + [[nodiscard]] QString accessToken() const noexcept + { + return minecraftAccessToken.accessToken; + } + + /** + * Profile UUID for game launch. + */ + [[nodiscard]] QString profileId() const noexcept + { + return profile.id; + } + + /** + * Profile name for game launch. + */ + [[nodiscard]] QString profileName() const noexcept + { + return profile.name; + } + + /** + * Xbox user hash (uhs) from token metadata. + */ + [[nodiscard]] QString xboxUserHash() const noexcept + { + return xboxUserToken.metadata.value(QStringLiteral("uhs")).toString(); + } + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.cpp new file mode 100644 index 0000000000..48433e81f6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.cpp @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "DeviceCodeAuthStep.hpp" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "Json.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + // Device authorization endpoints + constexpr auto kDeviceCodeUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + constexpr auto kTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + + /** + * Parse device code response from Microsoft. + */ + struct DeviceCodeResponse + { + QString deviceCode; + QString userCode; + QString verificationUri; + int expiresIn = 0; + int interval = 5; + QString error; + QString errorDescription; + + [[nodiscard]] bool isValid() const noexcept + { + return !deviceCode.isEmpty() && !userCode.isEmpty() && !verificationUri.isEmpty() && expiresIn > 0; + } + }; + + [[nodiscard]] DeviceCodeResponse parseDeviceCodeResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse device code response:" << err.errorString(); + return {}; + } + + const auto obj = doc.object(); + return { Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), + Json::ensureString(obj, "verification_uri"), Json::ensureInteger(obj, "expires_in"), + Json::ensureInteger(obj, "interval", 5), Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description") }; + } + + /** + * Parse token response from Microsoft. + */ + struct TokenResponse + { + QString accessToken; + QString tokenType; + QString refreshToken; + int expiresIn = 0; + QString error; + QString errorDescription; + QVariantMap metadata; + + [[nodiscard]] bool isSuccess() const noexcept + { + return !accessToken.isEmpty(); + } + [[nodiscard]] bool isPending() const noexcept + { + return error == QStringLiteral("authorization_pending"); + } + [[nodiscard]] bool needsSlowDown() const noexcept + { + return error == QStringLiteral("slow_down"); + } + }; + + [[nodiscard]] TokenResponse parseTokenResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse token response:" << err.errorString(); + return {}; + } + + const auto obj = doc.object(); + return { Json::ensureString(obj, "access_token"), + Json::ensureString(obj, "token_type"), + Json::ensureString(obj, "refresh_token"), + Json::ensureInteger(obj, "expires_in"), + Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description"), + obj.toVariantMap() }; + } + + } // namespace + + DeviceCodeAuthStep::DeviceCodeAuthStep(Credentials& credentials) noexcept + : Step(credentials), + m_clientId(APPLICATION->getMSAClientID()) + { + m_pollTimer.setTimerType(Qt::VeryCoarseTimer); + m_pollTimer.setSingleShot(true); + m_expirationTimer.setTimerType(Qt::VeryCoarseTimer); + m_expirationTimer.setSingleShot(true); + + connect(&m_expirationTimer, &QTimer::timeout, this, &DeviceCodeAuthStep::cancel); + connect(&m_pollTimer, &QTimer::timeout, this, &DeviceCodeAuthStep::pollForCompletion); + } + + QString DeviceCodeAuthStep::description() const + { + return tr("Logging in with Microsoft account (device code)."); + } + + void DeviceCodeAuthStep::execute() + { + QUrlQuery query; + query.addQueryItem(QStringLiteral("client_id"), m_clientId); + query.addQueryItem(QStringLiteral("scope"), QStringLiteral("XboxLive.SignIn XboxLive.offline_access")); + + const auto payload = query.query(QUrl::FullyEncoded).toUtf8(); + const QUrl url(QString::fromLatin1(kDeviceCodeUrl)); + + const auto headers = QList{ { "Content-Type", "application/x-www-form-urlencoded" }, + { "Accept", "application/json" } }; + + m_response = std::make_shared(); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("DeviceCodeRequest"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &DeviceCodeAuthStep::onDeviceCodeReceived); + m_task->start(); + } + + void DeviceCodeAuthStep::cancel() noexcept + { + m_cancelled = true; + m_expirationTimer.stop(); + m_pollTimer.stop(); + + if (m_request) + { + m_request->abort(); + } + + emit completed(StepResult::HardFailure, tr("Authentication cancelled or timed out.")); + } + + void DeviceCodeAuthStep::onDeviceCodeReceived() + { + const auto rsp = parseDeviceCodeResponse(*m_response); + + if (!rsp.error.isEmpty()) + { + const QString msg = rsp.errorDescription.isEmpty() ? rsp.error : rsp.errorDescription; + emit completed(StepResult::HardFailure, tr("Device authorization failed: %1").arg(msg)); + return; + } + + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) + { + emit completed(StepResult::HardFailure, tr("Failed to request device authorization.")); + return; + } + + if (!rsp.isValid()) + { + emit completed(StepResult::HardFailure, tr("Invalid device authorization response.")); + return; + } + + m_deviceCode = rsp.deviceCode; + m_pollInterval = rsp.interval > 0 ? rsp.interval : 5; + + // Notify UI to display code + emit deviceCodeReady(rsp.verificationUri, rsp.userCode, rsp.expiresIn); + + // Start polling + startPolling(m_pollInterval, rsp.expiresIn); + } + + void DeviceCodeAuthStep::startPolling(int intervalSecs, int expiresInSecs) + { + if (m_cancelled) + { + return; + } + + m_expirationTimer.setInterval(expiresInSecs * 1000); + m_expirationTimer.start(); + + m_pollTimer.setInterval(intervalSecs * 1000); + m_pollTimer.start(); + } + + void DeviceCodeAuthStep::pollForCompletion() + { + if (m_cancelled) + { + return; + } + + QUrlQuery query; + query.addQueryItem(QStringLiteral("client_id"), m_clientId); + query.addQueryItem(QStringLiteral("grant_type"), + QStringLiteral("urn:ietf:params:oauth:grant-type:device_code")); + query.addQueryItem(QStringLiteral("device_code"), m_deviceCode); + + const auto payload = query.query(QUrl::FullyEncoded).toUtf8(); + const QUrl url(QString::fromLatin1(kTokenUrl)); + + const auto headers = QList{ { "Content-Type", "application/x-www-form-urlencoded" }, + { "Accept", "application/json" } }; + + m_response = std::make_shared(); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request->setNetwork(APPLICATION->network()); + + connect(m_request.get(), &Task::finished, this, &DeviceCodeAuthStep::onPollResponse); + m_request->start(); + } + + void DeviceCodeAuthStep::onPollResponse() + { + if (m_cancelled) + { + return; + } + + // Handle timeout - exponential backoff per RFC 8628 + if (m_request->error() == QNetworkReply::TimeoutError) + { + m_pollInterval *= 2; + m_pollTimer.setInterval(m_pollInterval * 1000); + m_pollTimer.start(); + return; + } + + const auto rsp = parseTokenResponse(*m_response); + + // Handle slow_down - increase interval by 5 seconds per RFC 8628 + if (rsp.needsSlowDown()) + { + m_pollInterval += 5; + m_pollTimer.setInterval(m_pollInterval * 1000); + m_pollTimer.start(); + return; + } + + // Authorization still pending - keep polling + if (rsp.isPending()) + { + m_pollTimer.start(); + return; + } + + // Check for other errors + if (!rsp.error.isEmpty()) + { + const QString msg = rsp.errorDescription.isEmpty() ? rsp.error : rsp.errorDescription; + emit completed(StepResult::HardFailure, tr("Device authentication failed: %1").arg(msg)); + return; + } + + // Network error - retry + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) + { + m_pollTimer.start(); + return; + } + + // Success! + m_expirationTimer.stop(); + + m_credentials.msaClientId = m_clientId; + m_credentials.msaToken.issuedAt = QDateTime::currentDateTimeUtc(); + m_credentials.msaToken.expiresAt = QDateTime::currentDateTimeUtc().addSecs(rsp.expiresIn); + m_credentials.msaToken.metadata = rsp.metadata; + m_credentials.msaToken.refreshToken = rsp.refreshToken; + m_credentials.msaToken.accessToken = rsp.accessToken; + m_credentials.msaToken.validity = TokenValidity::Certain; + + emit completed(StepResult::Continue, tr("Microsoft authentication successful.")); + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.hpp new file mode 100644 index 0000000000..15d81d2e6b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.hpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Microsoft OAuth 2.0 Device Code Flow step. + * + * Used for environments where browser-based login is impractical. + * Displays a code for the user to enter at a Microsoft URL. + * + * Flow: + * 1. Request device code from Microsoft + * 2. Emit deviceCodeReady signal with code and URL + * 3. Poll for user completion + * 4. On success, populate MSA token in Credentials + */ + class DeviceCodeAuthStep : public Step + { + Q_OBJECT + + public: + explicit DeviceCodeAuthStep(Credentials& credentials) noexcept; + ~DeviceCodeAuthStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + void cancel() noexcept override; + + private slots: + void onDeviceCodeReceived(); + void pollForCompletion(); + void onPollResponse(); + + private: + void startPolling(int intervalSecs, int expiresInSecs); + + QString m_clientId; + QString m_deviceCode; + int m_pollInterval = 5; // seconds + bool m_cancelled = false; + + QTimer m_pollTimer; + QTimer m_expirationTimer; + + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.cpp new file mode 100644 index 0000000000..71058bd6ac --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.cpp @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "GameEntitlementsStep.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kEntitlementsUrl = "https://api.minecraftservices.com/entitlements/license"; + + } // namespace + + GameEntitlementsStep::GameEntitlementsStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString GameEntitlementsStep::description() const + { + return tr("Checking game ownership."); + } + + void GameEntitlementsStep::execute() + { + // Generate unique request ID for validation + m_requestId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + QUrl url(QString::fromLatin1(kEntitlementsUrl)); + url.setQuery(QStringLiteral("requestId=%1").arg(m_requestId)); + + const QString authHeader = QStringLiteral("Bearer %1").arg(m_credentials.minecraftAccessToken.accessToken); + + const auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", authHeader.toUtf8() } }; + + m_response = std::make_shared(); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("GameEntitlements"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &GameEntitlementsStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Checking game entitlements..."; + } + + void GameEntitlementsStep::onRequestCompleted() + { + qCDebug(authCredentials()) << *m_response; + + // Entitlements fetch is non-critical - continue even on failure + if (!parseEntitlementsResponse(*m_response)) + { + qWarning() << "Failed to parse entitlements response; continuing without entitlements."; + } + + emit completed(StepResult::Continue, tr("Got entitlements info.")); + } + + bool GameEntitlementsStep::parseEntitlementsResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse entitlements:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + // Validate request ID matches + const QString responseRequestId = obj.value(QStringLiteral("requestId")).toString(); + if (!responseRequestId.isEmpty() && responseRequestId != m_requestId) + { + qWarning() << "Entitlements request ID mismatch! Expected:" << m_requestId << "Got:" << responseRequestId; + } + + // Parse items array for Minecraft entitlements + const auto items = obj.value(QStringLiteral("items")).toArray(); + bool hasMinecraft = false; + bool hasGamePass = false; + + for (const auto& itemVal : items) + { + const auto itemObj = itemVal.toObject(); + const QString name = itemObj.value(QStringLiteral("name")).toString(); + + if (name == QStringLiteral("game_minecraft") || name == QStringLiteral("product_minecraft")) + { + hasMinecraft = true; + } + if (name == QStringLiteral("game_minecraft_bedrock")) + { + // Bedrock edition, not Java + } + if (name.contains(QStringLiteral("gamepass"), Qt::CaseInsensitive)) + { + hasGamePass = true; + } + } + + m_credentials.entitlements.ownsMinecraft = hasMinecraft || hasGamePass; + m_credentials.entitlements.canPlayMinecraft = hasMinecraft || hasGamePass; + m_credentials.entitlements.validity = TokenValidity::Certain; + + return true; + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.hpp new file mode 100644 index 0000000000..6b306107ca --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.hpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Game entitlements verification step. + * + * Fetches license/entitlement information to verify game ownership. + * This determines whether the user owns Minecraft and can play it. + * + * Endpoint: https://api.minecraftservices.com/entitlements/license + */ + class GameEntitlementsStep : public Step + { + Q_OBJECT + + public: + explicit GameEntitlementsStep(Credentials& credentials) noexcept; + ~GameEntitlementsStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + [[nodiscard]] bool parseEntitlementsResponse(const QByteArray& data); + + QString m_requestId; + + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.cpp new file mode 100644 index 0000000000..a0ad612481 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.cpp @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "MicrosoftOAuthStep.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "BuildConfig.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + /** + * Custom OOB reply handler that forwards OAuth callbacks from the application. + */ + class CustomSchemeReplyHandler : public QOAuthOobReplyHandler + { + Q_OBJECT + + public: + explicit CustomSchemeReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent) + { + connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); + } + + ~CustomSchemeReplyHandler() override + { + disconnect(APPLICATION, + &Application::oauthReplyRecieved, + this, + &QOAuthOobReplyHandler::callbackReceived); + } + + [[nodiscard]] QString callback() const override + { + return BuildConfig.LAUNCHER_APP_BINARY_NAME + QStringLiteral("://oauth/microsoft"); + } + }; + + /** + * Check if the custom URL scheme handler is registered with the OS AND + * that the registered handler points to the currently running binary. + * + * This is important when multiple builds of the launcher coexist on the same + * machine (e.g. an installed release and a locally compiled dev build). + * If the registered handler points to a *different* binary, the OAuth callback + * URL would be intercepted by that other instance instead of the current one, + * causing the login flow to fail silently. In that case we fall back to the + * HTTP loopback server handler which is always self-contained. + */ + [[nodiscard]] bool isCustomSchemeRegistered() + { +#ifdef Q_OS_LINUX + QProcess process; + process.start(QStringLiteral("xdg-mime"), + { QStringLiteral("query"), + QStringLiteral("default"), + QStringLiteral("x-scheme-handler/") + BuildConfig.LAUNCHER_APP_BINARY_NAME }); + process.waitForFinished(); + const QString output = process.readAllStandardOutput().trimmed(); + if (!output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME)) + return false; + + // Also verify the registered .desktop entry resolves to our own binary. + // xdg-mime returns something like "projtlauncher.desktop"; locate it and + // read the Exec= line to compare against our own executable path. + const QString desktopFileName = output.section(QLatin1Char('\n'), 0, 0).trimmed(); + const QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + for (const QString& dataDir : dataDirs) + { + const QString desktopPath = dataDir + QStringLiteral("/applications/") + desktopFileName; + QSettings desktopFile(desktopPath, QSettings::IniFormat); + desktopFile.beginGroup(QStringLiteral("Desktop Entry")); + const QString execLine = desktopFile.value(QStringLiteral("Exec")).toString(); + desktopFile.endGroup(); + if (execLine.isEmpty()) + continue; + // Exec= may contain %U or similar; take only the binary part. + const QString registeredBin = execLine.section(QLatin1Char(' '), 0, 0); + const QFileInfo currentBin(QCoreApplication::applicationFilePath()); + const QFileInfo registeredBinInfo(registeredBin); + if (registeredBinInfo.canonicalFilePath() == currentBin.canonicalFilePath()) + return true; + // Registered handler is a different binary → do not use custom scheme. + qDebug() << "Custom URL scheme is registered for a different binary (" << registeredBin + << ") — falling back to HTTP loopback handler."; + return false; + } + return true; // Could not verify; assume it's ours. +#elif defined(Q_OS_WIN) + const QString regPath = + QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); + const QSettings settings(regPath, QSettings::NativeFormat); + if (!settings.contains(QStringLiteral("shell/open/command/."))) + return false; + + // Verify that the registered command actually points to this binary. + // The registry value looks like: "C:\path\to\launcher.exe" "%1" + QString registeredCmd = settings.value(QStringLiteral("shell/open/command/.")).toString(); + // Strip surrounding quotes from the executable portion. + if (registeredCmd.startsWith(QLatin1Char('"'))) + { + registeredCmd = registeredCmd.mid(1); + const int closeQuote = registeredCmd.indexOf(QLatin1Char('"')); + if (closeQuote >= 0) + registeredCmd = registeredCmd.left(closeQuote); + } + else + { + // No quotes — executable ends at the first space. + const int spaceIdx = registeredCmd.indexOf(QLatin1Char(' ')); + if (spaceIdx >= 0) + registeredCmd = registeredCmd.left(spaceIdx); + } + + const QFileInfo currentBin(QCoreApplication::applicationFilePath()); + const QFileInfo registeredBin(registeredCmd); + if (registeredBin.canonicalFilePath().compare(currentBin.canonicalFilePath(), Qt::CaseInsensitive) == 0) + return true; + + // The URL scheme is registered, but for a different launcher binary. + // Fall back to the HTTP loopback handler so our OAuth callback reaches us. + qDebug() << "Custom URL scheme is registered for a different binary (" << registeredCmd + << ") — falling back to HTTP loopback handler."; + return false; +#else + return true; +#endif + } + + } // namespace + + MicrosoftOAuthStep::MicrosoftOAuthStep(Credentials& credentials, bool silentRefresh) noexcept + : Step(credentials), + m_silentRefresh(silentRefresh), + m_clientId(APPLICATION->getMSAClientID()) + { + setupOAuthHandlers(); + } + + QString MicrosoftOAuthStep::description() const + { + return m_silentRefresh ? tr("Refreshing Microsoft account token.") : tr("Logging in with Microsoft account."); + } + + void MicrosoftOAuthStep::setupOAuthHandlers() + { + // Choose appropriate reply handler based on environment + if (shouldUseCustomScheme()) + { + m_oauth.setReplyHandler(new CustomSchemeReplyHandler(this)); + } + else + { + auto* httpHandler = new QOAuthHttpServerReplyHandler(this); + httpHandler->setCallbackText(QStringLiteral(R"XXX( + + Login Successful, redirecting... + + )XXX") + .arg(BuildConfig.LOGIN_CALLBACK_URL)); + m_oauth.setReplyHandler(httpHandler); + } + + // Configure OAuth endpoints + m_oauth.setAuthorizationUrl( + QUrl(QStringLiteral("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"))); + m_oauth.setAccessTokenUrl( + QUrl(QStringLiteral("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"))); + m_oauth.setScope(QStringLiteral("XboxLive.SignIn XboxLive.offline_access")); + m_oauth.setClientIdentifier(m_clientId); + m_oauth.setNetworkAccessManager(APPLICATION->network().get()); + + // Connect signals + connect(&m_oauth, &QOAuth2AuthorizationCodeFlow::granted, this, &MicrosoftOAuthStep::onGranted); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, + this, + &MicrosoftOAuthStep::openBrowserRequested); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, + this, + &MicrosoftOAuthStep::browserAuthRequired); + connect(&m_oauth, &QOAuth2AuthorizationCodeFlow::requestFailed, this, &MicrosoftOAuthStep::onRequestFailed); + connect(&m_oauth, &QOAuth2AuthorizationCodeFlow::error, this, &MicrosoftOAuthStep::onError); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::extraTokensChanged, + this, + &MicrosoftOAuthStep::onExtraTokensChanged); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, + this, + &MicrosoftOAuthStep::onClientIdChanged); + } + + bool MicrosoftOAuthStep::shouldUseCustomScheme() const + { + // Use HTTP server handler for AppImage, portable, or unregistered scheme + const bool isAppImage = QCoreApplication::applicationFilePath().startsWith(QStringLiteral("/tmp/.mount_")); + const bool isPortable = APPLICATION->isPortable(); + return !isAppImage && !isPortable && isCustomSchemeRegistered(); + } + + void MicrosoftOAuthStep::execute() + { + if (m_silentRefresh) + { + // Validate preconditions for silent refresh + if (m_credentials.msaClientId != m_clientId) + { + emit completed(StepResult::Disabled, tr("Microsoft client ID has changed. Please log in again.")); + return; + } + + if (!m_credentials.msaToken.hasRefreshToken()) + { + emit completed(StepResult::Disabled, tr("No refresh token available. Please log in again.")); + return; + } + + m_oauth.setRefreshToken(m_credentials.msaToken.refreshToken); + m_oauth.refreshAccessToken(); + } + else + { + // Interactive login - clear existing credentials + m_credentials = Credentials{}; + m_credentials.msaClientId = m_clientId; + + // Force account selection prompt + m_oauth.setModifyParametersFunction( + [](QAbstractOAuth::Stage, QMultiMap* params) + { params->insert(QStringLiteral("prompt"), QStringLiteral("select_account")); }); + + m_oauth.grant(); + } + } + + void MicrosoftOAuthStep::onGranted() + { + m_credentials.msaClientId = m_oauth.clientIdentifier(); + m_credentials.msaToken.issuedAt = QDateTime::currentDateTimeUtc(); + m_credentials.msaToken.expiresAt = m_oauth.expirationAt(); + m_credentials.msaToken.metadata = m_oauth.extraTokens(); + m_credentials.msaToken.refreshToken = m_oauth.refreshToken(); + m_credentials.msaToken.accessToken = m_oauth.token(); + m_credentials.msaToken.validity = TokenValidity::Certain; + + emit completed(StepResult::Continue, tr("Microsoft authentication successful.")); + } + + void MicrosoftOAuthStep::onRequestFailed(QAbstractOAuth2::Error err) + { + StepResult result = StepResult::HardFailure; + + if (m_oauth.status() == QAbstractOAuth::Status::Granted || m_silentRefresh) + { + result = (err == QAbstractOAuth2::Error::NetworkError) ? StepResult::Offline : StepResult::SoftFailure; + } + + const QString message = + m_silentRefresh ? tr("Failed to refresh Microsoft token.") : tr("Microsoft authentication failed."); + qWarning() << message; + emit completed(result, message); + } + + void MicrosoftOAuthStep::onError(const QString& error, const QString& errorDescription, const QUrl& /*uri*/) + { + qWarning() << "OAuth error:" << error << "-" << errorDescription; + emit completed(StepResult::HardFailure, errorDescription.isEmpty() ? error : errorDescription); + } + + void MicrosoftOAuthStep::onExtraTokensChanged(const QVariantMap& tokens) + { + m_credentials.msaToken.metadata = tokens; + } + + void MicrosoftOAuthStep::onClientIdChanged(const QString& clientId) + { + m_credentials.msaClientId = clientId; + } + +} // namespace projt::minecraft::auth + +#include "MicrosoftOAuthStep.moc" diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.hpp new file mode 100644 index 0000000000..16df0da21a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.hpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Step.hpp" + +namespace projt::minecraft::auth +{ + + /** + * Microsoft OAuth 2.0 Authorization Code Flow step. + * + * Handles interactive browser-based login or silent token refresh using + * the standard OAuth 2.0 authorization code flow. Upon success, populates + * the MSA token in Credentials. + * + * This step supports two modes: + * - Interactive: Opens browser for user login + * - Silent: Attempts refresh using stored refresh token + */ + class MicrosoftOAuthStep : public Step + { + Q_OBJECT + + public: + /** + * Construct a new MSA OAuth step. + * @param credentials Credentials to populate with MSA token. + * @param silentRefresh If true, attempt silent refresh; if false, interactive login. + */ + explicit MicrosoftOAuthStep(Credentials& credentials, bool silentRefresh = false) noexcept; + ~MicrosoftOAuthStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + signals: + /** + * Emitted when browser authorization is required (interactive mode). + * @param url URL to open in user's browser. + */ + void openBrowserRequested(const QUrl& url); + + private slots: + void onGranted(); + void onRequestFailed(QAbstractOAuth2::Error error); + void onError(const QString& error, const QString& errorDescription, const QUrl& uri); + void onExtraTokensChanged(const QVariantMap& tokens); + void onClientIdChanged(const QString& clientId); + + private: + void setupOAuthHandlers(); + [[nodiscard]] bool shouldUseCustomScheme() const; + + bool m_silentRefresh = false; + QString m_clientId; + QOAuth2AuthorizationCodeFlow m_oauth; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.cpp new file mode 100644 index 0000000000..5529337ef5 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "MinecraftProfileFetchStep.hpp" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kMinecraftProfileUrl = "https://api.minecraftservices.com/minecraft/profile"; + + } // namespace + + MinecraftProfileFetchStep::MinecraftProfileFetchStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString MinecraftProfileFetchStep::description() const + { + return tr("Fetching Minecraft profile."); + } + + void MinecraftProfileFetchStep::execute() + { + const QUrl url(QString::fromLatin1(kMinecraftProfileUrl)); + const QString authHeader = QStringLiteral("Bearer %1").arg(m_credentials.minecraftAccessToken.accessToken); + + const auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", authHeader.toUtf8() } }; + + m_response = std::make_shared(); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("MinecraftProfileFetch"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &MinecraftProfileFetchStep::onRequestCompleted); + m_task->start(); + } + + void MinecraftProfileFetchStep::onRequestCompleted() + { + // 404 = no profile exists (valid state for new accounts) + if (m_request->error() == QNetworkReply::ContentNotFoundError) + { + m_credentials.profile = MinecraftJavaProfile{}; + emit completed(StepResult::Continue, tr("Account has no Minecraft profile.")); + return; + } + + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Minecraft profile fetch error:"; + qWarning() << " HTTP Status:" << m_request->replyStatusCode(); + qWarning() << " Error:" << m_request->error() << m_request->errorString(); + qWarning() << " Response:" << QString::fromUtf8(*m_response); + + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Failed to fetch Minecraft profile: %1").arg(m_request->errorString())); + return; + } + + if (!parseProfileResponse(*m_response)) + { + m_credentials.profile = MinecraftJavaProfile{}; + emit completed(StepResult::SoftFailure, tr("Could not parse Minecraft profile response.")); + return; + } + + emit completed(StepResult::Continue, tr("Got Minecraft profile.")); + } + + bool MinecraftProfileFetchStep::parseProfileResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse Minecraft profile:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + // Basic profile info + m_credentials.profile.id = obj.value(QStringLiteral("id")).toString(); + m_credentials.profile.name = obj.value(QStringLiteral("name")).toString(); + + if (m_credentials.profile.id.isEmpty()) + { + return false; + } + + // Parse skins + const auto skins = obj.value(QStringLiteral("skins")).toArray(); + for (const auto& skinVal : skins) + { + const auto skinObj = skinVal.toObject(); + const QString state = skinObj.value(QStringLiteral("state")).toString(); + + if (state == QStringLiteral("ACTIVE")) + { + m_credentials.profile.skin.id = skinObj.value(QStringLiteral("id")).toString(); + m_credentials.profile.skin.url = skinObj.value(QStringLiteral("url")).toString(); + m_credentials.profile.skin.variant = skinObj.value(QStringLiteral("variant")).toString(); + break; + } + } + + // Parse capes + const auto capes = obj.value(QStringLiteral("capes")).toArray(); + for (const auto& capeVal : capes) + { + const auto capeObj = capeVal.toObject(); + const QString capeId = capeObj.value(QStringLiteral("id")).toString(); + + PlayerCape cape; + cape.id = capeId; + cape.url = capeObj.value(QStringLiteral("url")).toString(); + cape.alias = capeObj.value(QStringLiteral("alias")).toString(); + + m_credentials.profile.capes.insert(capeId, cape); + + // Track active cape + const QString state = capeObj.value(QStringLiteral("state")).toString(); + if (state == QStringLiteral("ACTIVE")) + { + m_credentials.profile.activeCapeId = capeId; + } + } + + m_credentials.profile.validity = TokenValidity::Certain; + return true; + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.hpp new file mode 100644 index 0000000000..f9da7960e2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.hpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Minecraft Java profile fetch step. + * + * Fetches the Minecraft Java Edition profile (UUID, username, skins, capes). + * A profile may not exist if the user hasn't bought the game or set up + * their profile name yet. + * + * Endpoint: https://api.minecraftservices.com/minecraft/profile + */ + class MinecraftProfileFetchStep : public Step + { + Q_OBJECT + + public: + explicit MinecraftProfileFetchStep(Credentials& credentials) noexcept; + ~MinecraftProfileFetchStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + [[nodiscard]] bool parseProfileResponse(const QByteArray& data); + + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.cpp new file mode 100644 index 0000000000..eba64cc6f0 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "MinecraftServicesLoginStep.hpp" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kMinecraftLoginUrl = "https://api.minecraftservices.com/launcher/login"; + + /** + * Parse Minecraft services authentication response. + */ + [[nodiscard]] bool parseMinecraftAuthResponse(const QByteArray& data, AuthToken& token) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse Minecraft login response:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + token.accessToken = obj.value(QStringLiteral("access_token")).toString(); + token.issuedAt = QDateTime::currentDateTimeUtc(); + + const int expiresIn = obj.value(QStringLiteral("expires_in")).toInt(); + if (expiresIn > 0) + { + token.expiresAt = token.issuedAt.addSecs(expiresIn); + } + + // Store token type and other metadata + token.metadata.insert(QStringLiteral("token_type"), obj.value(QStringLiteral("token_type")).toString()); + + token.validity = TokenValidity::Certain; + return !token.accessToken.isEmpty(); + } + + } // namespace + + MinecraftServicesLoginStep::MinecraftServicesLoginStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString MinecraftServicesLoginStep::description() const + { + return tr("Logging in to Minecraft services."); + } + + void MinecraftServicesLoginStep::execute() + { + const QString uhs = m_credentials.minecraftServicesToken.metadata.value(QStringLiteral("uhs")).toString(); + const QString xToken = m_credentials.minecraftServicesToken.accessToken; + + const QString requestBody = QStringLiteral(R"({ + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" + })") + .arg(uhs, xToken); + + const QUrl url(QString::fromLatin1(kMinecraftLoginUrl)); + const auto headers = + QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" } }; + + m_response = std::make_shared(); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("MinecraftServicesLogin"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &MinecraftServicesLoginStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Getting Minecraft access token..."; + } + + void MinecraftServicesLoginStep::onRequestCompleted() + { + qCDebug(authCredentials()) << *m_response; + + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Minecraft login error:" << m_request->error(); + + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); + return; + } + + if (!parseMinecraftAuthResponse(*m_response, m_credentials.minecraftAccessToken)) + { + qWarning() << "Could not parse Minecraft login response"; + emit completed(StepResult::SoftFailure, tr("Failed to parse Minecraft access token response.")); + return; + } + + emit completed(StepResult::Continue, tr("Got Minecraft access token.")); + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.hpp new file mode 100644 index 0000000000..95428b6c9a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.hpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Minecraft Services login step. + * + * Exchanges the XSTS token for a Minecraft access token (Yggdrasil-style). + * This token is used to authenticate with Minecraft game servers. + * + * Endpoint: https://api.minecraftservices.com/launcher/login + */ + class MinecraftServicesLoginStep : public Step + { + Q_OBJECT + + public: + explicit MinecraftServicesLoginStep(Credentials& credentials) noexcept; + ~MinecraftServicesLoginStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.cpp new file mode 100644 index 0000000000..e4d7283a37 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "SkinDownloadStep.hpp" + +#include + +#include "Application.h" + +namespace projt::minecraft::auth +{ + + SkinDownloadStep::SkinDownloadStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString SkinDownloadStep::description() const + { + return tr("Downloading player skin."); + } + + void SkinDownloadStep::execute() + { + // Skip if no skin URL available + if (m_credentials.profile.skin.url.isEmpty()) + { + emit completed(StepResult::Continue, tr("No skin to download.")); + return; + } + + const QUrl url(m_credentials.profile.skin.url); + + m_response = std::make_shared(); + m_request = Net::Download::makeByteArray(url, m_response); + + m_task = NetJob::Ptr::create(QStringLiteral("SkinDownload"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &SkinDownloadStep::onRequestCompleted); + m_task->start(); + } + + void SkinDownloadStep::onRequestCompleted() + { + // Skin download is optional - always continue regardless of result + if (m_request->error() == QNetworkReply::NoError) + { + m_credentials.profile.skin.imageData = *m_response; + emit completed(StepResult::Continue, tr("Got player skin.")); + } + else + { + qWarning() << "Failed to download skin:" << m_request->errorString(); + emit completed(StepResult::Continue, tr("Skin download failed (continuing).")); + } + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.hpp new file mode 100644 index 0000000000..c94fe825cd --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.hpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Skin image download step. + * + * Downloads the player's skin image data from the URL in the profile. + * This is an optional step used for display in the launcher UI. + */ + class SkinDownloadStep : public Step + { + Q_OBJECT + + public: + explicit SkinDownloadStep(Credentials& credentials) noexcept; + ~SkinDownloadStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/Step.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/Step.hpp new file mode 100644 index 0000000000..7afb7159cb --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/Step.hpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" + +namespace projt::minecraft::auth +{ + + /** + * Result state for authentication pipeline steps. + * Each step emits one of these states upon completion. + */ + enum class StepResult + { + Continue, ///< Step succeeded, proceed to next step + Succeeded, ///< Final success - authentication complete + Offline, ///< Network unavailable - soft failure, can retry + SoftFailure, ///< Recoverable error - partial auth, can retry + HardFailure, ///< Unrecoverable error - tokens invalid + Disabled, ///< Account disabled (e.g., client ID changed) + Gone ///< Account no longer exists + }; + + // Forward declaration + struct Credentials; + + /** + * Abstract base class for authentication pipeline steps. + * + * Each step performs a discrete authentication action (e.g., OAuth exchange, + * token validation, profile fetch) and emits `completed` when done. + * + * Steps are designed to be stateless between runs - all persistent data + * is stored in the Credentials object passed at construction. + */ + class Step : public QObject + { + Q_OBJECT + + public: + using Ptr = shared_qobject_ptr; + + /** + * Construct a step with a reference to the credential store. + * @param credentials Mutable reference to authentication data. + */ + explicit Step(Credentials& credentials) noexcept : QObject(nullptr), m_credentials(credentials) + {} + + ~Step() noexcept override = default; + + // Rule of Zero - no copy/move (QObject constraint) + Step(const Step&) = delete; + Step& operator=(const Step&) = delete; + Step(Step&&) = delete; + Step& operator=(Step&&) = delete; + + /** + * Human-readable description of what this step does. + * Used for progress display and logging. + */ + [[nodiscard]] virtual QString description() const = 0; + + public slots: + /** + * Execute this authentication step. + * Implementations must emit `completed` when done (success or failure). + */ + virtual void execute() = 0; + + /** + * Request cancellation of an in-progress step. + * Default implementation does nothing. Override for cancellable steps. + */ + virtual void cancel() noexcept + {} + + signals: + /** + * Emitted when the step completes (successfully or with error). + * @param result The outcome of this step. + * @param message Human-readable status message. + */ + void completed(StepResult result, QString message); + + /** + * Emitted by OAuth steps when browser authorization is required. + * @param url The URL to open in the user's browser. + */ + void browserAuthRequired(const QUrl& url); + + /** + * Emitted by device code flow steps when user action is required. + * @param verificationUrl URL to visit for authentication. + * @param userCode Code to enter at the URL. + * @param expiresInSecs Seconds until the code expires. + */ + void deviceCodeReady(QString verificationUrl, QString userCode, int expiresInSecs); + + protected: + Credentials& m_credentials; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/Steps.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/Steps.hpp new file mode 100644 index 0000000000..4cb8b8b07b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/Steps.hpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** + * @file Steps.hpp + * @brief Convenience header including all authentication pipeline steps. + * + * Include this header to get access to all step types: + * - MicrosoftOAuthStep: Browser-based MSA login + * - DeviceCodeAuthStep: Device code flow for console/headless + * - XboxLiveUserStep: XBL user token acquisition + * - XboxSecurityTokenStep: XSTS token for services + * - XboxProfileFetchStep: Xbox profile (optional) + * - MinecraftServicesLoginStep: Minecraft access token + * - MinecraftProfileFetchStep: Minecraft profile + * - GameEntitlementsStep: Game ownership check + * - SkinDownloadStep: Player skin image + */ + +#pragma once + +#include "Credentials.hpp" +#include "Step.hpp" + +#include "DeviceCodeAuthStep.hpp" +#include "GameEntitlementsStep.hpp" +#include "MicrosoftOAuthStep.hpp" +#include "MinecraftProfileFetchStep.hpp" +#include "MinecraftServicesLoginStep.hpp" +#include "SkinDownloadStep.hpp" +#include "XboxLiveUserStep.hpp" +#include "XboxProfileFetchStep.hpp" +#include "XboxSecurityTokenStep.hpp" diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.cpp new file mode 100644 index 0000000000..ef48d4ab47 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.cpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "XboxLiveUserStep.hpp" + +#include +#include +#include + +#include "Application.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kXboxUserAuthUrl = "https://user.auth.xboxlive.com/user/authenticate"; + + /** + * Parse Xbox token response. + * Returns true on success, false on parse error. + */ + [[nodiscard]] bool parseXboxTokenResponse(const QByteArray& data, AuthToken& token, const QString& tokenName) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse" << tokenName << "response:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + // Parse issue and expiry times + const QString issued = obj.value(QStringLiteral("IssueInstant")).toString(); + const QString expires = obj.value(QStringLiteral("NotAfter")).toString(); + token.issuedAt = QDateTime::fromString(issued, Qt::ISODate); + token.expiresAt = QDateTime::fromString(expires, Qt::ISODate); + token.accessToken = obj.value(QStringLiteral("Token")).toString(); + + // Parse display claims for user hash (uhs) + const auto displayClaims = obj.value(QStringLiteral("DisplayClaims")).toObject(); + const auto xui = displayClaims.value(QStringLiteral("xui")).toArray(); + if (!xui.isEmpty()) + { + const auto firstClaim = xui.first().toObject(); + token.metadata.insert(QStringLiteral("uhs"), firstClaim.value(QStringLiteral("uhs")).toString()); + } + + token.validity = TokenValidity::Certain; + + if (token.accessToken.isEmpty()) + { + qWarning() << "Empty" << tokenName << "token received"; + return false; + } + + return true; + } + + } // namespace + + XboxLiveUserStep::XboxLiveUserStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString XboxLiveUserStep::description() const + { + return tr("Authenticating with Xbox Live."); + } + + void XboxLiveUserStep::execute() + { + const QString requestBody = QStringLiteral(R"({ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + "RpsTicket": "d=%1" + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" + })") + .arg(m_credentials.msaToken.accessToken); + + const QUrl url(QString::fromLatin1(kXboxUserAuthUrl)); + const auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "x-xbl-contract-version", "1" } }; + + m_response = std::make_shared(); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("XboxLiveUserAuth"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxLiveUserStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Authenticating with Xbox Live..."; + } + + void XboxLiveUserStep::onRequestCompleted() + { + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Xbox Live user auth error:" << m_request->error(); + + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Xbox Live authentication failed: %1").arg(m_request->errorString())); + return; + } + + if (!parseXboxTokenResponse(*m_response, m_credentials.xboxUserToken, QStringLiteral("User"))) + { + emit completed(StepResult::SoftFailure, tr("Could not parse Xbox Live user token response.")); + return; + } + + emit completed(StepResult::Continue, tr("Got Xbox Live user token.")); + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.hpp new file mode 100644 index 0000000000..a8c0514599 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.hpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Xbox Live User Authentication step. + * + * Exchanges the MSA token for an Xbox Live user token. + * This is the first step of Xbox authentication after MSA login. + * + * Endpoint: https://user.auth.xboxlive.com/user/authenticate + */ + class XboxLiveUserStep : public Step + { + Q_OBJECT + + public: + explicit XboxLiveUserStep(Credentials& credentials) noexcept; + ~XboxLiveUserStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.cpp new file mode 100644 index 0000000000..8660d4291f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "XboxProfileFetchStep.hpp" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kXboxProfileUrl = "https://profile.xboxlive.com/users/me/profile/settings"; + + // Profile settings to request + constexpr auto kProfileSettings = "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag," + "ModernGamertagSuffix,UniqueModernGamertag,AccountTier,TenureLevel," + "XboxOneRep,PreferredColor,Location,Bio,Watermarks,RealName," + "RealNameOverride,IsQuarantined"; + + } // namespace + + XboxProfileFetchStep::XboxProfileFetchStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString XboxProfileFetchStep::description() const + { + return tr("Fetching Xbox profile."); + } + + void XboxProfileFetchStep::execute() + { + QUrl url(QString::fromLatin1(kXboxProfileUrl)); + QUrlQuery query; + query.addQueryItem(QStringLiteral("settings"), QString::fromLatin1(kProfileSettings)); + url.setQuery(query); + + const QString authHeader = QStringLiteral("XBL3.0 x=%1;%2") + .arg(m_credentials.xboxUserHash(), m_credentials.xboxServiceToken.accessToken); + + const auto headers = QList{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "x-xbl-contract-version", "3" }, + { "Authorization", authHeader.toUtf8() } }; + + m_response = std::make_shared(); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("XboxProfileFetch"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxProfileFetchStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Fetching Xbox profile..."; + } + + void XboxProfileFetchStep::onRequestCompleted() + { + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Xbox profile fetch error:" << m_request->error(); + qCDebug(authCredentials()) << *m_response; + + // Profile fetch is optional - continue even on failure + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Failed to fetch Xbox profile: %1").arg(m_request->errorString())); + return; + } + + qCDebug(authCredentials()) << "Xbox profile:" << *m_response; + + // Parse the response to extract gamertag + parseProfileResponse(); + + emit completed(StepResult::Continue, tr("Got Xbox profile.")); + } + + void XboxProfileFetchStep::parseProfileResponse() + { + QJsonParseError parseError; + const auto doc = QJsonDocument::fromJson(*m_response, &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse Xbox profile response:" << parseError.errorString(); + return; + } + + const auto root = doc.object(); + const auto profileUsers = root.value(QStringLiteral("profileUsers")).toArray(); + + if (profileUsers.isEmpty()) + { + qWarning() << "No profile users in Xbox response"; + return; + } + + const auto user = profileUsers.first().toObject(); + const auto settings = user.value(QStringLiteral("settings")).toArray(); + + for (const auto& settingValue : settings) + { + const auto setting = settingValue.toObject(); + const auto id = setting.value(QStringLiteral("id")).toString(); + const auto value = setting.value(QStringLiteral("value")).toString(); + + if (id == QStringLiteral("Gamertag")) + { + // Store gamertag in xboxServiceToken.metadata for legacy sync + // accountDisplayString() expects this in xboxApiToken.extra["gtg"] + m_credentials.xboxServiceToken.metadata[QStringLiteral("gtg")] = value; + qDebug() << "Got Xbox gamertag:" << value; + } + else if (id == QStringLiteral("GameDisplayPicRaw")) + { + m_credentials.xboxServiceToken.metadata[QStringLiteral("gamerPicUrl")] = value; + } + } + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.hpp new file mode 100644 index 0000000000..35a7594acd --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.hpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Xbox Live profile fetch step. + * + * Fetches the Xbox Live profile (gamertag, avatar, etc.) for logging + * and display purposes. This is an optional/informational step. + * + * Endpoint: https://profile.xboxlive.com/users/me/profile/settings + */ + class XboxProfileFetchStep : public Step + { + Q_OBJECT + + public: + explicit XboxProfileFetchStep(Credentials& credentials) noexcept; + ~XboxProfileFetchStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + void parseProfileResponse(); + + std::shared_ptr m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.cpp new file mode 100644 index 0000000000..6ed455c6b1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "XboxSecurityTokenStep.hpp" + +#include +#include +#include +#include + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kXstsAuthorizeUrl = "https://xsts.auth.xboxlive.com/xsts/authorize"; + + /** + * Parse Xbox token response. + */ + [[nodiscard]] bool parseXboxTokenResponse(const QByteArray& data, AuthToken& token) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse XSTS response:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + token.issuedAt = QDateTime::fromString(obj.value(QStringLiteral("IssueInstant")).toString(), Qt::ISODate); + token.expiresAt = QDateTime::fromString(obj.value(QStringLiteral("NotAfter")).toString(), Qt::ISODate); + token.accessToken = obj.value(QStringLiteral("Token")).toString(); + + const auto displayClaims = obj.value(QStringLiteral("DisplayClaims")).toObject(); + const auto xui = displayClaims.value(QStringLiteral("xui")).toArray(); + if (!xui.isEmpty()) + { + const auto firstClaim = xui.first().toObject(); + token.metadata.insert(QStringLiteral("uhs"), firstClaim.value(QStringLiteral("uhs")).toString()); + } + + token.validity = TokenValidity::Certain; + return !token.accessToken.isEmpty(); + } + + /** + * XSTS error code messages. + * See: https://wiki.vg/Microsoft_Authentication_Scheme#Authenticate_with_XSTS + */ + struct XstsErrorInfo + { + int64_t code; + const char* message; + }; + + constexpr std::array kXstsErrors = { + XstsErrorInfo{ 2148916227, "This Microsoft account was banned by Xbox." }, + XstsErrorInfo{ 2148916229, "This account is restricted. Please check parental controls." }, + XstsErrorInfo{ 2148916233, "This account does not have an Xbox Live profile. Purchase the game first." }, + XstsErrorInfo{ 2148916234, "Please accept Xbox Terms of Service and try again." }, + XstsErrorInfo{ 2148916235, "Xbox Live is not available in your region." }, + XstsErrorInfo{ 2148916236, "This account requires age verification." }, + XstsErrorInfo{ 2148916237, "This account has reached its playtime limit." }, + XstsErrorInfo{ 2148916238, "This account is underaged and not linked to a family." } + }; + + } // namespace + + XboxSecurityTokenStep::XboxSecurityTokenStep(Credentials& credentials, XstsTarget target) noexcept + : Step(credentials), + m_target(target) + {} + + QString XboxSecurityTokenStep::description() const + { + return tr("Getting authorization for %1 services.").arg(targetName()); + } + + QString XboxSecurityTokenStep::relyingParty() const + { + switch (m_target) + { + case XstsTarget::XboxLive: return QStringLiteral("http://xboxlive.com"); + case XstsTarget::MinecraftServices: return QStringLiteral("rp://api.minecraftservices.com/"); + } + Q_UNREACHABLE(); + } + + QString XboxSecurityTokenStep::targetName() const + { + switch (m_target) + { + case XstsTarget::XboxLive: return QStringLiteral("Xbox Live"); + case XstsTarget::MinecraftServices: return QStringLiteral("Minecraft"); + } + Q_UNREACHABLE(); + } + + AuthToken& XboxSecurityTokenStep::targetToken() + { + switch (m_target) + { + case XstsTarget::XboxLive: return m_credentials.xboxServiceToken; + case XstsTarget::MinecraftServices: return m_credentials.minecraftServicesToken; + } + Q_UNREACHABLE(); + } + + void XboxSecurityTokenStep::execute() + { + const QString requestBody = QStringLiteral(R"({ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": ["%1"] + }, + "RelyingParty": "%2", + "TokenType": "JWT" + })") + .arg(m_credentials.xboxUserToken.accessToken, relyingParty()); + + const QUrl url(QString::fromLatin1(kXstsAuthorizeUrl)); + const auto headers = + QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" } }; + + m_response = std::make_shared(); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("XstsAuthorize"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxSecurityTokenStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Getting XSTS token for" << relyingParty(); + } + + void XboxSecurityTokenStep::onRequestCompleted() + { + qCDebug(authCredentials()) << *m_response; + + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "XSTS request error:" << m_request->error(); + + if (Net::isApplicationError(m_request->error())) + { + if (handleStsError()) + { + return; + } + emit completed(StepResult::SoftFailure, + tr("Failed to get %1 authorization: %2").arg(targetName(), m_request->errorString())); + } + else + { + emit completed(StepResult::Offline, + tr("Failed to get %1 authorization: %2").arg(targetName(), m_request->errorString())); + } + return; + } + + AuthToken token; + if (!parseXboxTokenResponse(*m_response, token)) + { + emit completed(StepResult::SoftFailure, tr("Could not parse %1 authorization response.").arg(targetName())); + return; + } + + // Verify user hash matches + const QString responseUhs = token.metadata.value(QStringLiteral("uhs")).toString(); + if (responseUhs != m_credentials.xboxUserHash()) + { + emit completed(StepResult::SoftFailure, tr("User hash mismatch in %1 authorization.").arg(targetName())); + return; + } + + targetToken() = token; + emit completed(StepResult::Continue, tr("Got %1 authorization.").arg(targetName())); + } + + bool XboxSecurityTokenStep::handleStsError() + { + if (m_request->error() != QNetworkReply::AuthenticationRequiredError) + { + return false; + } + + QJsonParseError jsonError; + const auto doc = QJsonDocument::fromJson(*m_response, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + emit completed(StepResult::SoftFailure, + tr("Cannot parse XSTS error response: %1").arg(jsonError.errorString())); + return true; + } + + const auto obj = doc.object(); + const auto errorCode = static_cast(obj.value(QStringLiteral("XErr")).toDouble()); + + if (errorCode == 0) + { + emit completed(StepResult::SoftFailure, tr("XSTS error response missing error code.")); + return true; + } + + // Look up error message + for (const auto& [code, message] : kXstsErrors) + { + if (code == errorCode) + { + emit completed(StepResult::SoftFailure, tr(message)); + return true; + } + } + + emit completed(StepResult::SoftFailure, tr("Unknown XSTS error: %1").arg(errorCode)); + return true; + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.hpp new file mode 100644 index 0000000000..911e343728 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.hpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include + +#include "Credentials.hpp" +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Target services for XSTS token requests. + */ + enum class XstsTarget + { + XboxLive, ///< Xbox Live services (for profile fetch) + MinecraftServices ///< Minecraft services (for game access) + }; + + /** + * Xbox Security Token Service (XSTS) authorization step. + * + * Requests an XSTS token for a specific relying party (service). + * Two instances are typically used in the auth pipeline: + * - One for Xbox Live services (profile) + * - One for Minecraft services (game access) + * + * Endpoint: https://xsts.auth.xboxlive.com/xsts/authorize + */ + class XboxSecurityTokenStep : public Step + { + Q_OBJECT + + public: + /** + * Construct an XSTS authorization step. + * @param credentials Credentials to populate. + * @param target Which service to request authorization for. + */ + explicit XboxSecurityTokenStep(Credentials& credentials, XstsTarget target) noexcept; + ~XboxSecurityTokenStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + [[nodiscard]] QString relyingParty() const; + [[nodiscard]] QString targetName() const; + [[nodiscard]] AuthToken& targetToken(); + bool handleStsError(); + + XstsTarget m_target; + + std::shared_ptr m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.cpp b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.cpp new file mode 100644 index 0000000000..518953be75 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.cpp @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "AutoInstallJava.hpp" +#include +#include +#include + +#include "Application.h" +#include "FileSystem.h" +#include "MessageLevel.h" +#include "QObjectPtr.h" +#include "SysInfo.h" +#include "java/core/RuntimeInstall.hpp" +#include "java/core/RuntimeVersion.hpp" +#include "java/services/RuntimeCatalog.hpp" +#include "java/services/RuntimeScanner.hpp" +#include "java/download/RuntimeArchiveTask.hpp" +#include "java/download/RuntimeManifestTask.hpp" +#include "java/download/RuntimeLinkTask.hpp" +#include "meta/Index.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/Mode.h" +#include "tasks/SequentialTask.h" + +AutoInstallJava::AutoInstallJava(projt::launch::LaunchPipeline* parent) + : projt::launch::LaunchStage(parent), + m_instance(m_flow->instance()), + m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; + +void AutoInstallJava::executeTask() +{ + auto settings = m_instance->settings(); + if (!APPLICATION->settings()->get("AutomaticJavaSwitch").toBool()) + { + emitSucceeded(); + return; + } + if (!settings->get("OverrideJavaLocation").toBool()) + { + emitSucceeded(); + return; + } + auto packProfile = m_instance->getPackProfile(); + const auto configuredJavaPath = settings->get("JavaPath").toString(); + const bool hasValidJava = !FS::ResolveExecutable(configuredJavaPath).isEmpty(); + const bool ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); + const auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); + bool compatibleJava = true; + if (!ignoreCompatibility && !compatibleMajors.isEmpty()) + { + auto storedVersion = settings->get("JavaVersion").toString(); + if (storedVersion.isEmpty()) + { + compatibleJava = false; + } + else + { + projt::java::RuntimeVersion javaVersion(storedVersion); + compatibleJava = compatibleMajors.contains(javaVersion.major()); + } + } + if (hasValidJava && compatibleJava) + { + emitSucceeded(); + return; + } + if (!APPLICATION->settings()->get("AutomaticJavaDownload").toBool()) + { + auto javas = APPLICATION->runtimeCatalog(); + m_current_task = javas->getLoadTask(); + connect( + m_current_task.get(), + &Task::finished, + this, + [this, javas, packProfile] + { + for (auto i = 0; i < javas->count(); i++) + { + auto java = std::dynamic_pointer_cast(javas->at(i)); + if (java && packProfile->getProfile()->getCompatibleJavaMajors().contains(java->version.major())) + { + if (!java->is_64bit) + { + emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), + MessageLevel::Launcher); + } + setJavaPath(java->path); + return; + } + } + emit logLine(tr("No compatible Java version was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + }); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + emit progressReportingRequest(); + return; + } + if (m_supported_arch.isEmpty()) + { + emit logLine( + tr("Your system (%1-%2) is not compatible with automatic Java installation. Using the default Java path.") + .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emitSucceeded(); + return; + } + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + if (wantedJavaName.isEmpty()) + { + emit logLine(tr("Your meta information is out of date or doesn't have the information necessary to determine " + "what installation of " + "Java should be used. " + "Using the default Java path."), + MessageLevel::Warning); + emitSucceeded(); + return; + } + QDir javaDir(APPLICATION->javaPath()); + auto wantedJavaPath = javaDir.absoluteFilePath(wantedJavaName); + if (QFileInfo::exists(wantedJavaPath)) + { + setJavaPathFromPartial(); + return; + } + auto versionList = APPLICATION->metadataIndex()->component("net.minecraft.java"); + m_current_task = versionList->getLoadTask(); + connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::emitFailed); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + if (!m_current_task->isRunning()) + { + m_current_task->start(); + } + emit progressReportingRequest(); +} + +void AutoInstallJava::setJavaPath(QString path) +{ + auto settings = m_instance->settings(); + settings->set("OverrideJavaLocation", true); + settings->set("JavaPath", path); + settings->set("AutomaticJava", true); + emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Launcher); + emitSucceeded(); +} + +void AutoInstallJava::setJavaPathFromPartial() +{ + auto packProfile = m_instance->getPackProfile(); + auto javaName = packProfile->getProfile()->getCompatibleJavaName(); + QDir javaDir(APPLICATION->javaPath()); + // just checking if the executable is there should suffice + // but if needed this can be achieved through refreshing the javalist + // and retrieving the path that contains the java name + auto relativeBinary = FS::PathCombine(javaName, "bin", projt::java::RuntimeScanner::executableName()); + auto finalPath = javaDir.absoluteFilePath(relativeBinary); + if (QFileInfo::exists(finalPath)) + { + setJavaPath(finalPath); + } + else + { + emit logLine( + tr("No compatible Java version was found (the binary file does not exist). Using the default one."), + MessageLevel::Warning); + emitSucceeded(); + } + return; +} + +void AutoInstallJava::downloadJava(projt::meta::MetaVersion::Ptr version, QString javaName) +{ + auto runtimes = version->detailedData()->runtimes; + for (auto java : runtimes) + { + if (java->runtimeOS == m_supported_arch && java->name() == javaName) + { + QDir javaDir(APPLICATION->javaPath()); + auto final_path = javaDir.absoluteFilePath(java->displayName); + switch (java->downloadKind) + { + case projt::java::PackageKind::Manifest: + m_current_task = makeShared(java->url, + final_path, + java->checksumType, + java->checksumHash); + break; + case projt::java::PackageKind::Archive: + m_current_task = makeShared(java->url, + final_path, + java->checksumType, + java->checksumHash); + break; + case projt::java::PackageKind::Unknown: + emitFailed(tr("Could not determine Java download type!")); + return; + } +#if defined(Q_OS_MACOS) + auto seq = makeShared(tr("Install Java")); + seq->addTask(m_current_task); + seq->addTask(makeShared(final_path)); + m_current_task = seq; +#endif + auto deletePath = [final_path] { FS::deletePath(final_path); }; + connect(m_current_task.get(), + &Task::failed, + this, + [this, deletePath](QString reason) + { + deletePath(); + emitFailed(reason); + }); + connect(m_current_task.get(), &Task::aborted, this, [deletePath] { deletePath(); }); + connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::setJavaPathFromPartial); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + m_current_task->start(); + return; + } + } + tryNextMajorJava(); +} + +void AutoInstallJava::tryNextMajorJava() +{ + if (!isRunning()) + return; + auto versionList = APPLICATION->metadataIndex()->component("net.minecraft.java"); + auto packProfile = m_instance->getPackProfile(); + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + auto majorJavaVersions = packProfile->getProfile()->getCompatibleJavaMajors(); + if (m_majorJavaVersionIndex >= majorJavaVersions.length()) + { + emit logLine(tr("No versions of Java were found for your operating system: %1-%2") + .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emit logLine(tr("No compatible version of Java was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + return; + } + auto majorJavaVersion = majorJavaVersions[m_majorJavaVersionIndex]; + m_majorJavaVersionIndex++; + + auto javaMajor = versionList->getMetaVersion(QString("java%1").arg(majorJavaVersion)); + if (!javaMajor) + { + tryNextMajorJava(); + return; + } + + if (javaMajor->isFullyLoaded()) + { + downloadJava(javaMajor, wantedJavaName); + } + else + { + m_current_task = APPLICATION->metadataIndex()->loadVersionTask("net.minecraft.java", + javaMajor->versionId(), + Net::Mode::Online); + connect(m_current_task.get(), + &Task::succeeded, + this, + [this, javaMajor, wantedJavaName] { downloadJava(javaMajor, wantedJavaName); }); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + if (!m_current_task->isRunning()) + { + m_current_task->start(); + } + } +} +bool AutoInstallJava::abort() +{ + if (m_current_task && m_current_task->canAbort()) + { + auto status = m_current_task->abort(); + emitAborted(); + return status; + } + return Task::abort(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.hpp b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.hpp new file mode 100644 index 0000000000..93fa50818e --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.hpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include "meta/Version.hpp" +#include "minecraft/MinecraftInstance.h" +#include "tasks/Task.h" + +class AutoInstallJava : public projt::launch::LaunchStage +{ + Q_OBJECT + + public: + explicit AutoInstallJava(projt::launch::LaunchPipeline* parent); + ~AutoInstallJava() override = default; + + void executeTask() override; + bool canAbort() const override + { + return m_current_task ? m_current_task->canAbort() : false; + } + bool abort() override; + + protected: + void setJavaPath(QString path); + void setJavaPathFromPartial(); + void downloadJava(projt::meta::MetaVersion::Ptr version, QString javaName); + void tryNextMajorJava(); + + private: + MinecraftInstancePtr m_instance; + Task::Ptr m_current_task; + + qsizetype m_majorJavaVersionIndex = 0; + const QString m_supported_arch; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.cpp b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.cpp new file mode 100644 index 0000000000..2b9150214d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ClaimAccount.hpp" +#include + +#include "Application.h" +#include "minecraft/auth/AccountList.hpp" + +ClaimAccount::ClaimAccount(projt::launch::LaunchPipeline* parent, AuthSessionPtr session) + : projt::launch::LaunchStage(parent) +{ + if (session->launchMode == LaunchMode::Normal) + { + auto accounts = APPLICATION->accounts(); + m_account = accounts->getAccountByProfileName(session->player_name); + } +} + +void ClaimAccount::executeTask() +{ + if (m_account) + { + lock.reset(new UseLock(m_account)); + } + emitSucceeded(); +} + +void ClaimAccount::finalize() +{ + lock.reset(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.hpp b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.hpp new file mode 100644 index 0000000000..448e77745f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include + +class ClaimAccount : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ClaimAccount(projt::launch::LaunchPipeline* parent, AuthSessionPtr session); + virtual ~ClaimAccount() = default; + + void executeTask() override; + void finalize() override; + bool canAbort() const override + { + return false; + } + + private: + std::unique_ptr lock; + MinecraftAccountPtr m_account; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.cpp b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.cpp new file mode 100644 index 0000000000..039ec5fe03 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "CreateGameFolders.hpp" +#include "FileSystem.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" + +CreateGameFolders::CreateGameFolders(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) +{} + +void CreateGameFolders::executeTask() +{ + auto instance = m_flow->instance(); + + if (!FS::ensureFolderPathExists(instance->gameRoot())) + { + emit logLine("Couldn't create the main game folder", MessageLevel::Error); + emitFailed(tr("Couldn't create the main game folder")); + return; + } + + // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. + if (!FS::ensureFolderPathExists(FS::PathCombine(instance->gameRoot(), "server-resource-packs"))) + { + emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); + } + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.hpp b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.hpp new file mode 100644 index 0000000000..68c5973165 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.hpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +// Create the main .minecraft for the instance and any other necessary folders +class CreateGameFolders : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit CreateGameFolders(projt::launch::LaunchPipeline* parent); + virtual ~CreateGameFolders() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.cpp b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.cpp new file mode 100644 index 0000000000..4ac7e484dc --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.cpp @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "ExtractNatives.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include "FileSystem.h" +#include "MMCZip.h" + +#ifdef major +#undef major +#endif +#ifdef minor +#undef minor +#endif + +static QString replaceSuffix(QString target, const QString& suffix, const QString& replacement) +{ + if (!target.endsWith(suffix)) + { + return target; + } + target.resize(target.length() - suffix.length()); + return target + replacement; +} + +static bool isWithinExtractionRoot(const QDir& root, const QString& path) +{ + const auto cleanRoot = QDir::cleanPath(root.absolutePath()); + const auto cleanPath = QDir::cleanPath(path); + return cleanPath == cleanRoot || cleanPath.startsWith(cleanRoot + '/'); +} + +static bool symlinkEscapesExtractionRoot(QuaZip& zip, const QString& outputPath, const QDir& root) +{ + QuaZipFileInfo64 info; + if (!zip.getCurrentFileInfo(&info) || !info.isSymbolicLink()) + { + return false; + } + + QuaZipFile linkFile(&zip); + if (!linkFile.open(QIODevice::ReadOnly)) + { + return true; + } + + const auto linkTarget = QFile::decodeName(linkFile.readAll()); + linkFile.close(); + + QString resolvedTarget; + if (QDir::isAbsolutePath(linkTarget)) + { + resolvedTarget = QDir::cleanPath(linkTarget); + } + else + { + const auto outputDir = QFileInfo(outputPath).dir(); + resolvedTarget = QDir::cleanPath(outputDir.absoluteFilePath(linkTarget)); + } + + return !isWithinExtractionRoot(root, resolvedTarget); +} + +static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) +{ + QuaZip zip(source); + if (!zip.open(QuaZip::mdUnzip)) + { + return false; + } + QDir directory(targetFolder); + if (!zip.goToFirstFile()) + { + return false; + } + do + { + QString name = zip.getCurrentFileName(); + auto lowercase = name.toLower(); + if (applyJnilibHack) + { + name = replaceSuffix(name, ".jnilib", ".dylib"); + } + QString absFilePath = directory.absoluteFilePath(name); + if (symlinkEscapesExtractionRoot(zip, absFilePath, directory)) + { + return false; + } + if (!JlCompress::extractFile(&zip, "", absFilePath)) + { + return false; + } + } + while (zip.goToNextFile()); + zip.close(); + if (zip.getZipError() != 0) + { + return false; + } + return true; +} + +void ExtractNatives::executeTask() +{ + auto instance = m_flow->instance(); + auto toExtract = instance->getNativeJars(); + if (toExtract.isEmpty()) + { + emitSucceeded(); + return; + } + auto settings = instance->settings(); + + auto outputPath = instance->getNativePath(); + FS::ensureFolderPathExists(outputPath); + auto javaVersion = instance->getRuntimeVersion(); + bool jniHackEnabled = javaVersion.major() >= 8; + for (const auto& source : toExtract) + { + if (!unzipNatives(source, outputPath, jniHackEnabled)) + { + const char* reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); + emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); + emitFailed(tr(reason).arg(source, outputPath)); + } + } + emitSucceeded(); +} + +void ExtractNatives::finalize() +{ + auto instance = m_flow->instance(); + QString target_dir = FS::PathCombine(instance->instanceRoot(), "natives/"); + QDir dir(target_dir); + dir.removeRecursively(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.hpp b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.hpp new file mode 100644 index 0000000000..696c1ac6bf --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include + +class ExtractNatives : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ExtractNatives(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ExtractNatives() {}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } + void finalize() override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.cpp b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.cpp new file mode 100644 index 0000000000..642189d4ad --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LauncherPartLaunch.hpp" + +#include +#include +#include + +#include "Application.h" +#include "Commandline.h" +#include "FileSystem.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" + +#ifdef Q_OS_LINUX +#include "gamemode_client.h" +#endif + +LauncherPartLaunch::LauncherPartLaunch(projt::launch::LaunchPipeline* parent) + : projt::launch::LaunchStage(parent), + m_process(parent->instance()->getRuntimeVersion().defaultsToUtf8() ? QStringConverter::Utf8 + : QStringConverter::System) +{ + if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) + { + static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption); + std::shared_ptr connection{ new QMetaObject::Connection }; + *connection = connect(&m_process, + &LoggedProcess::log, + this, + [connection](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) + { + qDebug() << lines; + if (lines.filter(s_settingUser).length() != 0) + { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); + } + + connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); +} + +void LauncherPartLaunch::executeTask() +{ + QString jarPath = APPLICATION->getJarPath("ProjTLaunch.jar"); + if (jarPath.isEmpty()) + { + const char* reason = QT_TR_NOOP("Launcher library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + + auto instance = m_flow->instance(); + + QString legacyJarPath; + if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) + { + legacyJarPath = APPLICATION->getJarPath("ProjTLaunchLegacy.jar"); + if (legacyJarPath.isEmpty()) + { + const char* reason = + QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + } + + m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); + QStringList args = instance->javaArguments(); + QString allArgs = args.join(", "); + emit logLine("Java Arguments:\n[" + m_flow->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); + + auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); + + m_process.setProcessEnvironment(instance->createLaunchEnvironment()); + + // make detachable - this will keep the process running even if the object is destroyed + m_process.setDetachable(true); + + auto classPath = instance->getClassPath(); + classPath.prepend(jarPath); + + if (!legacyJarPath.isEmpty()) + classPath.prepend(legacyJarPath); + + auto natPath = instance->getNativePath(); +#ifdef Q_OS_WIN + natPath = FS::getPathNameInLocal8bit(natPath); +#endif + args << "-Djava.library.path=" + natPath; + + args << "-cp"; +#ifdef Q_OS_WIN + QStringList processed; + for (auto& item : classPath) + { + processed << FS::getPathNameInLocal8bit(item); + } + args << processed.join(';'); +#else + args << classPath.join(':'); +#endif + args << "org.projecttick.projtlauncher.normal.EntryPoint"; + + qDebug() << args.join(' '); + + QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); + if (!wrapperCommandStr.isEmpty()) + { + wrapperCommandStr = m_flow->substituteVariables(wrapperCommandStr); + auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr); + auto wrapperCommand = wrapperArgs.takeFirst(); + auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); + if (realWrapperCommand.isEmpty()) + { + const char* reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); + return; + } + emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::Launcher); + args.prepend(javaPath); + m_process.start(wrapperCommand, wrapperArgs + args); + } + else + { + m_process.start(javaPath, args); + } + +#ifdef Q_OS_LINUX + if (instance->settings()->get("EnableFeralGamemode").toBool() + && APPLICATION->capabilities() & Application::SupportsGameMode) + { + auto pid = m_process.processId(); + if (pid) + { + gamemode_request_start_for(pid); + } + } +#endif +} + +void LauncherPartLaunch::on_state(LoggedProcess::State state) +{ + switch (state) + { + case LoggedProcess::FailedToStart: + { + //: Error message displayed if instace can't start + const char* reason = QT_TR_NOOP("Could not launch Minecraft!"); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + { + m_flow->setPid(-1); + m_flow->instance()->setMinecraftRunning(false); + emitFailed(tr("Game crashed.")); + return; + } + case LoggedProcess::Finished: + { + auto instance = m_flow->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) + APPLICATION->showMainWindow(); + + m_flow->setPid(-1); + m_flow->instance()->setMinecraftRunning(false); + // if the exit code wasn't 0, report this as a crash + auto exitCode = m_process.exitCode(); + if (exitCode != 0) + { + emitFailed(tr("Game crashed.")); + return; + } + + // Set exit code in environment for post-launch hooks + auto env = m_process.processEnvironment(); + env.insert("INST_EXITCODE", QString::number(exitCode)); + env.insert("INST_NAME", instance->name()); + env.insert("INST_ID", instance->id()); + env.insert("INST_DIR", instance->instanceRoot()); + // Post-launch command can be retrieved from instance settings + QString postExitCmd = instance->settings()->get("PostExitCommand").toString(); + if (!postExitCmd.isEmpty()) + { + emit logLine(tr("Running post-exit command: %1").arg(postExitCmd), MessageLevel::Launcher); + QProcess postProcess; + postProcess.setProcessEnvironment(env); + postProcess.setWorkingDirectory(instance->instanceRoot()); + postProcess.start(postExitCmd); + postProcess.waitForFinished(30000); // 30 second timeout + if (postProcess.exitCode() != 0) + { + emit logLine(tr("Post-exit command failed with code %1").arg(postProcess.exitCode()), + MessageLevel::Warning); + } + } + + emitSucceeded(); + break; + } + case LoggedProcess::Running: + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher); + m_flow->setPid(m_process.processId()); + // send the launch script to the launcher part + m_process.write(m_launchScript.toUtf8()); + + mayProceed = true; + emit readyForLaunch(); + break; + default: break; + } +} + +void LauncherPartLaunch::setWorkingDirectory(const QString& wd) +{ + m_process.setWorkingDirectory(wd); +} + +void LauncherPartLaunch::proceed() +{ + if (mayProceed) + { + m_flow->instance()->setMinecraftRunning(true); + QString launchString("launch\n"); + m_process.write(launchString.toUtf8()); + mayProceed = false; + } +} + +bool LauncherPartLaunch::abort() +{ + if (mayProceed) + { + mayProceed = false; + QString launchString("abort\n"); + m_process.write(launchString.toUtf8()); + } + else + { + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.hpp b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.hpp new file mode 100644 index 0000000000..0160980391 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.hpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +#include "MinecraftTarget.hpp" + +class LauncherPartLaunch : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit LauncherPartLaunch(projt::launch::LaunchPipeline* parent); + virtual ~LauncherPartLaunch() = default; + + virtual void executeTask(); + virtual bool abort(); + virtual void proceed(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString& wd); + void setAuthSession(AuthSessionPtr session) + { + m_session = session; + } + + void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) + { + m_targetToJoin = std::move(targetToJoin); + } + + private slots: + void on_state(LoggedProcess::State state); + + private: + LoggedProcess m_process; + QString m_command; + AuthSessionPtr m_session; + QString m_launchScript; + MinecraftTarget::Ptr m_targetToJoin; + + bool mayProceed = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.cpp b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.cpp new file mode 100644 index 0000000000..5590ac05ed --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#include "MinecraftTarget.hpp" + +#include +#include + +// Note: This parser intentionally mirrors Minecraft's address parsing behavior exactly, +// including its tolerance for malformed input. Invalid addresses resolve to unusable +// targets, which Minecraft handles at connection time. The isValid() method can be +// used by callers requiring validation. +MinecraftTarget MinecraftTarget::parse(const QString& fullAddress, bool useWorld) +{ + // Validate input - empty or whitespace-only addresses are invalid + QString trimmed = fullAddress.trimmed(); + if (trimmed.isEmpty()) + { + return MinecraftTarget{}; // Return empty target for invalid input + } + + if (useWorld) + { + MinecraftTarget target; + target.world = trimmed; + return target; + } + + QStringList split = trimmed.split(":"); + + // The logic below replicates the exact logic minecraft uses for parsing server addresses. + // While the conversion is not lossless and eats errors, it ensures the same behavior + // within Minecraft and ProjT Launcher when entering server addresses. + if (trimmed.startsWith("[")) + { + int bracket = trimmed.indexOf("]"); + if (bracket > 0) + { + QString ipv6 = trimmed.mid(1, bracket - 1); + QString port = trimmed.mid(bracket + 1).trimmed(); + + if (port.startsWith(":") && !ipv6.isEmpty()) + { + port = port.mid(1); + split = QStringList({ ipv6, port }); + } + else + { + split = QStringList({ ipv6 }); + } + } + } + + if (split.size() > 2) + { + split = QStringList({ trimmed }); + } + + QString realAddress = split[0]; + + // Validate address is not empty after parsing + if (realAddress.isEmpty()) + { + return MinecraftTarget{}; // Invalid address + } + + quint16 realPort = 25565; + if (split.size() > 1) + { + bool ok; + uint portValue = split[1].toUInt(&ok); + + // Validate port is in valid range (1-65535) + if (ok && portValue > 0 && portValue <= 65535) + { + realPort = static_cast(portValue); + } + else + { + // Invalid port, use default + realPort = 25565; + } + } + + return MinecraftTarget{ realAddress, realPort }; +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.hpp b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.hpp new file mode 100644 index 0000000000..11ff4b9bab --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include + +#include + +struct MinecraftTarget +{ + QString address; + quint16 port; + + QString world; + static MinecraftTarget parse(const QString& fullAddress, bool useWorld); + + bool isValid() const + { + return !address.isEmpty() || !world.isEmpty(); + } + + using Ptr = std::shared_ptr; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.cpp b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.cpp new file mode 100644 index 0000000000..f635420498 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.cpp @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ModMinecraftJar.hpp" +#include "FileSystem.h" +#include "MMCZip.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +void ModMinecraftJar::executeTask() +{ + auto m_inst = m_flow->instance(); + + if (!m_inst->getJarMods().size()) + { + emitSucceeded(); + return; + } + // nuke obsolete stripped jar(s) if needed + if (!FS::ensureFolderPathExists(m_inst->binRoot())) + { + emitFailed(tr("Couldn't create the bin folder for Minecraft.jar")); + } + + auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); + if (!removeJar()) + { + emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); + } + + // create temporary modded jar, if needed + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto jarMods = m_inst->getJarMods(); + if (jarMods.size()) + { + auto mainJar = profile->getMainJar(); + QStringList jars, temp1, temp2, temp3, temp4; + mainJar->getApplicableFiles(m_inst->runtimeContext(), jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath()); + auto sourceJarPath = jars[0]; + if (!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) + { + emitFailed(tr("Failed to create the custom Minecraft jar file.")); + return; + } + } + emitSucceeded(); +} + +void ModMinecraftJar::finalize() +{ + removeJar(); +} + +bool ModMinecraftJar::removeJar() +{ + auto m_inst = m_flow->instance(); + auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); + QFile finalJar(finalJarPath); + if (finalJar.exists()) + { + if (!finalJar.remove()) + { + return false; + } + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.hpp b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.hpp new file mode 100644 index 0000000000..0dee222941 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.hpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include + +class ModMinecraftJar : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ModMinecraftJar(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ModMinecraftJar() {}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } + void finalize() override; + + private: + bool removeJar(); +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.cpp b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.cpp new file mode 100644 index 0000000000..186254353c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#include +#include +#include + +#include +#include +#include "PrintInstanceInfo.hpp" + +#include "HardwareInfo.h" + +#if defined(Q_OS_FREEBSD) +namespace +{ + void runSysctlHwModel(QStringList& log) + { + char buff[512]; + FILE* hwmodel = popen("sysctl hw.model", "r"); + while (fgets(buff, 512, hwmodel) != nullptr) + { + log << QString::fromUtf8(buff); + break; + } + pclose(hwmodel); + } + + void runPciconf(QStringList& log) + { + char buff[512]; + std::string strcard; + FILE* pciconf = popen("pciconf -lv -a vgapci0", "r"); + while (fgets(buff, 512, pciconf) != nullptr) + { + if (strncmp(buff, " vendor", 10) == 0) + { + std::string str(buff); + strcard.append( + str.substr(str.find_first_of("'") + 1, str.find_last_not_of("'") - (str.find_first_of("'") + 2))); + strcard.append(" "); + } + else if (strncmp(buff, " device", 10) == 0) + { + std::string str2(buff); + strcard.append( + str2.substr(str2.find_first_of("'") + 1, str2.find_last_not_of("'") - (str2.find_first_of("'") + 2))); + } + log << QString::fromStdString(strcard); + break; + } + pclose(pciconf); + } +} // namespace +#endif + +void PrintInstanceInfo::executeTask() +{ + auto instance = m_flow->instance(); + QStringList log; + + log << "OS: " + QString("%1 | %2 | %3").arg(QSysInfo::prettyProductName(), QSysInfo::kernelType(), QSysInfo::kernelVersion()); +#ifdef Q_OS_FREEBSD + ::runSysctlHwModel(log); + ::runPciconf(log); +#else + log << "CPU: " + HardwareInfo::cpuInfo(); + log << QString("RAM: %1 MiB (available: %2 MiB)").arg(HardwareInfo::totalRamMiB()).arg(HardwareInfo::availableRamMiB()); +#endif + log.append(HardwareInfo::gpuInfo()); + log << ""; + + logLines(log, MessageLevel::Launcher); + logLines(instance->verboseDescription(m_session, m_targetToJoin), MessageLevel::Launcher); + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.hpp b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.hpp new file mode 100644 index 0000000000..10bf98a66d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.hpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include "minecraft/auth/AuthSession.hpp" +#include "minecraft/launch/MinecraftTarget.hpp" + +class PrintInstanceInfo : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit PrintInstanceInfo(projt::launch::LaunchPipeline* parent, + AuthSessionPtr session, + MinecraftTarget::Ptr targetToJoin) + : projt::launch::LaunchStage(parent), + m_session(session), + m_targetToJoin(targetToJoin) {}; + virtual ~PrintInstanceInfo() = default; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } + + private: + AuthSessionPtr m_session; + MinecraftTarget::Ptr m_targetToJoin; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.cpp b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.cpp new file mode 100644 index 0000000000..9f719f3539 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#include "ReconstructAssets.hpp" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/AssetsUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +void ReconstructAssets::executeTask() +{ + auto instance = m_flow->instance(); + auto components = instance->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + if (!AssetsUtils::reconstructAssets(assets->id, instance->resourcesDir())) + { + emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); + } + + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.hpp b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.hpp new file mode 100644 index 0000000000..715d78cbd4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include + +class ReconstructAssets : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ReconstructAssets(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ReconstructAssets() {}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.cpp b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.cpp new file mode 100644 index 0000000000..bad7feca25 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ScanModFolders.hpp" +#include "FileSystem.h" +#include "MMCZip.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/mod/ModFolderModel.hpp" + +void ScanModFolders::executeTask() +{ + auto m_inst = m_flow->instance(); + + auto loaders = m_inst->loaderModList(); + connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + if (!loaders->update()) + { + m_modsDone = true; + } + + auto cores = m_inst->coreModList(); + connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + if (!cores->update()) + { + m_coreModsDone = true; + } + + auto nils = m_inst->nilModList(); + connect(nils.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); + if (!nils->update()) + { + m_nilModsDone = true; + } + checkDone(); +} + +void ScanModFolders::modsDone() +{ + m_modsDone = true; + checkDone(); +} + +void ScanModFolders::coreModsDone() +{ + m_coreModsDone = true; + checkDone(); +} + +void ScanModFolders::nilModsDone() +{ + m_nilModsDone = true; + checkDone(); +} + +void ScanModFolders::checkDone() +{ + if (m_modsDone && m_coreModsDone && m_nilModsDone) + { + emitSucceeded(); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.hpp b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.hpp new file mode 100644 index 0000000000..13648fc9d6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.hpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include + +class ScanModFolders : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ScanModFolders(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ScanModFolders() {}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } + private slots: + void coreModsDone(); + void modsDone(); + void nilModsDone(); + + private: + void checkDone(); + + private: // DATA + bool m_modsDone = false; + bool m_nilModsDone = false; + bool m_coreModsDone = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.cpp b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.cpp new file mode 100644 index 0000000000..18c6d0efca --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "VerifyJavaInstall.hpp" +#include + +#include "Application.h" +#include "MessageLevel.h" +#include "java/core/RuntimeVersion.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +void VerifyJavaInstall::executeTask() +{ + auto instance = m_flow->instance(); + auto packProfile = instance->getPackProfile(); + auto settings = instance->settings(); + auto storedVersion = settings->get("JavaVersion").toString(); + auto ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); + auto javaArchitecture = settings->get("JavaArchitecture").toString(); + auto maxMemAlloc = settings->get("MaxMemAlloc").toInt(); + + if (javaArchitecture == "32" && maxMemAlloc > 2048) + { + emit logLine(tr("Max memory allocation exceeds the supported value.\n" + "The selected installation of Java is 32-bit and doesn't support more than 2048MiB of RAM.\n" + "The instance may not start due to this."), + MessageLevel::Error); + } + + auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); + + projt::java::RuntimeVersion javaVersion(storedVersion); + + if (compatibleMajors.isEmpty() || compatibleMajors.contains(javaVersion.major())) + { + emitSucceeded(); + return; + } + + if (ignoreCompatibility) + { + emit logLine(tr("Java major version is incompatible. Things might break."), MessageLevel::Warning); + emitSucceeded(); + return; + } + + emit logLine(tr("This instance is not compatible with Java version %1.\n" + "Please switch to one of the following Java versions for this instance:") + .arg(javaVersion.major()), + MessageLevel::Error); + for (auto major : compatibleMajors) + { + emit logLine(tr("Java version %1").arg(major), MessageLevel::Error); + } + emit logLine(tr("Go to instance Java settings to change your Java version or disable the Java compatibility check " + "if you know what " + "you're doing."), + MessageLevel::Error); + + emitFailed(QString("Incompatible Java major version")); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.hpp b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.hpp new file mode 100644 index 0000000000..ec5961e0a7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.hpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +class VerifyJavaInstall : public projt::launch::LaunchStage +{ + Q_OBJECT + + public: + explicit VerifyJavaInstall(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + ~VerifyJavaInstall() override = default; + + void executeTask() override; + bool canAbort() const override + { + return false; + } +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPack.cpp b/archived/projt-launcher/launcher/minecraft/mod/DataPack.cpp new file mode 100644 index 0000000000..5834a0e52a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPack.cpp @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "DataPack.hpp" + +#include +#include +#include + +#include "MTPixmapCache.h" +#include "Version.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.hpp" + +// Values taken from: +// https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats +static const QMap> s_pack_format_versions = { + { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, + { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, + { 8, { Version("1.18"), Version("1.18.1") } }, + { 9, { Version("1.18.2"), Version("1.18.2") } }, + { 10, { Version("1.19"), Version("1.19.3") } }, + { 11, { Version("23w03a"), Version("23w05a") } }, + { 12, { Version("1.19.4"), Version("1.19.4") } }, + { 13, { Version("23w12a"), Version("23w14a") } }, + { 14, { Version("23w16a"), Version("23w17a") } }, + { 15, { Version("1.20"), Version("1.20.1") } }, + { 16, { Version("23w31a"), Version("23w31a") } }, + { 17, { Version("23w32a"), Version("23w35a") } }, + { 18, { Version("1.20.2"), Version("1.20.2") } }, + { 19, { Version("23w40a"), Version("23w40a") } }, + { 20, { Version("23w41a"), Version("23w41a") } }, + { 21, { Version("23w42a"), Version("23w42a") } }, + { 22, { Version("23w43a"), Version("23w43b") } }, + { 23, { Version("23w44a"), Version("23w44a") } }, + { 24, { Version("23w45a"), Version("23w45a") } }, + { 25, { Version("23w46a"), Version("23w46a") } }, + { 26, { Version("1.20.3"), Version("1.20.4") } }, + { 27, { Version("23w51a"), Version("23w51b") } }, + { 28, { Version("24w05a"), Version("24w05b") } }, + { 29, { Version("24w04a"), Version("24w04a") } }, + { 30, { Version("24w05a"), Version("24w05b") } }, + { 31, { Version("24w06a"), Version("24w06a") } }, + { 32, { Version("24w07a"), Version("24w07a") } }, + { 33, { Version("24w09a"), Version("24w09a") } }, + { 34, { Version("24w10a"), Version("24w10a") } }, + { 35, { Version("24w11a"), Version("24w11a") } }, + { 36, { Version("24w12a"), Version("24w12a") } }, + { 37, { Version("24w13a"), Version("24w13a") } }, + { 38, { Version("24w14a"), Version("24w14a") } }, + { 39, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } }, + { 40, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } }, + { 41, { Version("1.20.5"), Version("1.20.6") } }, + { 42, { Version("24w18a"), Version("24w18a") } }, + { 43, { Version("24w19a"), Version("24w19b") } }, + { 44, { Version("24w20a"), Version("24w20a") } }, + { 45, { Version("21w21a"), Version("21w21b") } }, + { 46, { Version("1.21-pre1"), Version("1.21-pre1") } }, + { 47, { Version("1.21-pre2"), Version("1.21-pre2") } }, + { 48, { Version("1.21-pre3"), Version("1.21.1") } }, + { 49, { Version("24w33a"), Version("24w33a") } }, + { 50, { Version("24w34a"), Version("24w34a") } }, + { 51, { Version("24w35a"), Version("24w35a") } }, + { 52, { Version("24w36a"), Version("24w36a") } }, + { 53, { Version("24w37a"), Version("24w37a") } }, + { 54, { Version("24w38a"), Version("24w38a") } }, + { 55, { Version("24w39a"), Version("24w39a") } }, + { 56, { Version("24w40a"), Version("24w40a") } }, + { 57, { Version("1.21.2-pre1"), Version("1.21.3") } }, + { 58, { Version("24w44a"), Version("24w39a") } }, + { 59, { Version("24w45a"), Version("24w39a") } }, + { 60, { Version("24w46a"), Version("1.21.4-pre1") } }, + { 61, { Version("1.21.4-pre2"), Version("1.21.4") } }, + { 62, { Version("25w02a"), Version("25w02a") } }, + { 63, { Version("25w03a"), Version("25w03a") } }, + { 64, { Version("25w04a"), Version("25w04a") } }, + { 65, { Version("25w05a"), Version("25w05a") } }, + { 66, { Version("25w06a"), Version("25w06a") } }, + { 67, { Version("25w07a"), Version("25w07a") } }, + { 68, { Version("25w08a"), Version("25w08a") } }, + { 69, { Version("25w09a"), Version("25w09b") } }, + { 70, { Version("25w10a"), Version("1.21.5-pre1") } } +}; + +void DataPack::setPackFormat(int new_format_id) +{ + QMutexLocker locker(&m_data_lock); + + if (!s_pack_format_versions.contains(new_format_id)) + { + qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!"; + } + + m_pack_format = new_format_id; +} + +void DataPack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +void DataPack::setLocalizedDescription(const QString& langCode, const QString& description) +{ + QMutexLocker locker(&m_data_lock); + + if (langCode.isEmpty()) + { + m_description = description; + } + else + { + m_localized_descriptions[langCode] = description; + } +} + +void DataPack::setLocalizedDescriptions(const QHash& descriptions) +{ + QMutexLocker locker(&m_data_lock); + + m_localized_descriptions = descriptions; +} + +void DataPack::setImage(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::instance().remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage( + new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; + + // This can happen if the pixmap is too big to fit in the cache :c + if (!m_pack_image_cache_key.key.isValid()) + { + qWarning() << "Could not insert a image cache entry! Ignoring it."; + m_pack_image_cache_key.was_ever_used = false; + } +} + +QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) + { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode, Qt::SmoothTransformation); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) + { + return {}; + } + else + { + qDebug() << "Data Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + + // Imaged got evicted from the cache. Re-process it and retry. + DataPackUtils::processPackPNG(this); + return image(size); +} + +std::pair DataPack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) + { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} + +int DataPack::compare(const Resource& other, SortType type) const +{ + auto const& cast_other = static_cast(other); + if (type == SortType::PACK_FORMAT) + { + auto this_ver = packFormat(); + auto other_ver = cast_other.packFormat(); + + if (this_ver > other_ver) + return 1; + if (this_ver < other_ver) + return -1; + } + else + { + return Resource::compare(other, type); + } + return 0; +} + +bool DataPack::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + if (filter.match(QString::number(packFormat())).hasMatch()) + return true; + + if (filter.match(compatibleVersions().first.toString()).hasMatch()) + return true; + if (filter.match(compatibleVersions().second.toString()).hasMatch()) + return true; + + return Resource::applyFilter(filter); +} + +bool DataPack::valid() const +{ + return m_pack_format != 0; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPack.hpp b/archived/projt-launcher/launcher/minecraft/mod/DataPack.hpp new file mode 100644 index 0000000000..84bef9c0b7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPack.hpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +#include +#include +#include +#include + +class Version; + +class DataPack : public Resource +{ + Q_OBJECT + public: + DataPack(QObject* parent = nullptr) : Resource(parent) + {} + DataPack(QFileInfo file_info) : Resource(file_info) + {} + + /** Gets the numerical ID of the pack format. */ + int packFormat() const + { + return m_pack_format; + } + /** Gets, respectively, the lower and upper versions supported by the set pack format. */ + virtual std::pair compatibleVersions() const; + + /** Gets the description of the data pack. + * If a localized description exists for the current locale, returns that. + * Otherwise returns the default description. + */ + QString description() const + { + QString lang = QLocale::system().name().section('_', 0, 0); + if (m_localized_descriptions.contains(lang)) + { + return m_localized_descriptions.value(lang); + } + return m_description; + } + + /** Gets the raw (non-localized) description. */ + QString rawDescription() const + { + return m_description; + } + + /** Gets description for a specific language code. */ + QString localizedDescription(const QString& langCode) const + { + return m_localized_descriptions.value(langCode, m_description); + } + + /** Gets all available localized descriptions. */ + QHash allLocalizedDescriptions() const + { + return m_localized_descriptions; + } + + /** Gets the image of the data pack, converted to a QPixmap for drawing, and scaled to size. */ + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + + /** Thread-safe. */ + void setPackFormat(int new_format_id); + + /** Thread-safe. */ + void setDescription(QString new_description); + + /** Thread-safe. Sets a localized description for a specific language. */ + void setLocalizedDescription(const QString& langCode, const QString& description); + + /** Thread-safe. Sets all localized descriptions at once. */ + void setLocalizedDescriptions(const QHash& descriptions); + + /** Thread-safe. */ + void setImage(QImage new_image) const; + + bool valid() const override; + + [[nodiscard]] int compare(Resource const& other, SortType type) const override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + protected: + mutable QMutex m_data_lock; + + /* The 'version' of a data pack, as defined in the pack.mcmeta file. + * See https://minecraft.wiki/w/Data_pack#pack.mcmeta + */ + int m_pack_format = 0; + + /** The data pack's description, as defined in the pack.mcmeta file. + */ + QString m_description; + + /** Localized descriptions keyed by language code (e.g., "en", "de", "ja"). + * Parsed from pack.mcmeta language files if available. + */ + QHash m_localized_descriptions; + + /** The data pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct + { + QPixmapCache::Key key; + bool was_ever_used = false; + } mutable m_pack_image_cache_key; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.cpp new file mode 100644 index 0000000000..1248e62af4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "DataPackFolderModel.hpp" +#include +#include + +#include +#include + +#include "Version.h" + +#include "minecraft/mod/tasks/LocalDataPackParseTask.hpp" + +DataPackFolderModel::DataPackFolderModel(const QString& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); + m_column_names_translated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; + m_column_resize_modes = { QHeaderView::Interactive, + QHeaderView::Interactive, + QHeaderView::Stretch, + QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true }; +} + +QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + if (column == PackFormatColumn) + { + auto& resource = at(row); + auto pack_format = resource.packFormat(); + if (pack_format == 0) + return tr("Unrecognized"); + + auto version_bounds = resource.compatibleVersions(); + if (version_bounds.first.toString().isEmpty()) + return QString::number(pack_format); + + return QString("%1 (%2 - %3)") + .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + break; + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::ToolTipRole: + if (column == PackFormatColumn) + { + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case PackFormatColumn: + case DateColumn: + case ImageColumn: return columnNames().at(section); + default: return {}; + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: return tr("Is the data pack enabled? (Only valid for ZIPs)"); + case NameColumn: return tr("The name of the data pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: return tr("The date and time this data pack was last changed (or added)."); + default: return {}; + } + case Qt::SizeHintRole: + if (section == ImageColumn) + { + return QSize(64, 0); + } + return {}; + default: return {}; + } +} + +int DataPackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Resource* DataPackFolderModel::createResource(const QFileInfo& file) +{ + return new DataPack(file); +} + +Task* DataPackFolderModel::createParseTask(Resource& resource) +{ + return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast(&resource)); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.hpp new file mode 100644 index 0000000000..28b113bcb1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.hpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "ResourceFolderModel.hpp" + +#include "DataPack.hpp" +#include "ResourcePack.hpp" + +class DataPackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + PackFormatColumn, + DateColumn, + NUM_COLUMNS + }; + + explicit DataPackFolderModel(const QString& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + virtual QString id() const override + { + return "datapacks"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override; + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(DataPack) +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/MetadataHandler.hpp b/archived/projt-launcher/launcher/minecraft/mod/MetadataHandler.hpp new file mode 100644 index 0000000000..43c611bb66 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/MetadataHandler.hpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "modplatform/packwiz/Packwiz.h" + +namespace Metadata +{ + using ModStruct = Packwiz::V1::Mod; + + inline ModStruct create(const QDir& index_dir, + ModPlatform::IndexedPack& mod_pack, + ModPlatform::IndexedVersion& mod_version) + { + return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); + } + + inline void update(const QDir& index_dir, ModStruct& mod) + { + Packwiz::V1::updateModIndex(index_dir, mod); + } + + inline void remove(const QDir& index_dir, QString mod_slug) + { + Packwiz::V1::deleteModIndex(index_dir, mod_slug); + } + + inline ModStruct get(const QDir& index_dir, QString mod_slug) + { + return Packwiz::V1::getIndexForMod(index_dir, std::move(mod_slug)); + } + + inline ModStruct get(const QDir& index_dir, QVariant& mod_id) + { + return Packwiz::V1::getIndexForMod(index_dir, mod_id); + } + +}; // namespace Metadata diff --git a/archived/projt-launcher/launcher/minecraft/mod/Mod.cpp b/archived/projt-launcher/launcher/minecraft/mod/Mod.cpp new file mode 100644 index 0000000000..97aabac583 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Mod.cpp @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "Mod.hpp" +#include + +#include +#include +#include + +#include "MTPixmapCache.h" +#include "MetadataHandler.hpp" +#include "Resource.hpp" +#include "Version.h" +#include "minecraft/mod/ModDetails.hpp" +#include "minecraft/mod/tasks/LocalModParseTask.hpp" +#include "modplatform/ModIndex.h" + +Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() +{ + m_enabled = (file.suffix() != "disabled"); +} + +void Mod::setDetails(const ModDetails& details) +{ + m_local_details = details; +} + +int Mod::compare(const Resource& other, SortType type) const +{ + auto cast_other = dynamic_cast(&other); + if (!cast_other) + return Resource::compare(other, type); + + switch (type) + { + default: + case SortType::ENABLED: + case SortType::NAME: + case SortType::DATE: + case SortType::SIZE: return Resource::compare(other, type); + case SortType::VERSION: + { + auto this_ver = Version(version()); + auto other_ver = Version(cast_other->version()); + if (this_ver > other_ver) + return 1; + if (this_ver < other_ver) + return -1; + break; + } + case SortType::SIDE: + { + auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::MC_VERSIONS: + { + auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::LOADERS: + { + auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::RELEASE_TYPE: + { + auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + } + return 0; +} + +bool Mod::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + for (auto& author : authors()) + { + if (filter.match(author).hasMatch()) + { + return true; + } + } + + return Resource::applyFilter(filter); +} + +auto Mod::details() const -> const ModDetails& +{ + return m_local_details; +} + +auto Mod::name() const -> QString +{ + auto d_name = details().name; + if (!d_name.isEmpty()) + return d_name; + + return Resource::name(); +} + +auto Mod::mod_id() const -> QString +{ + auto d_mod_id = details().mod_id; + if (!d_mod_id.isEmpty()) + return d_mod_id; + + return Resource::name(); +} + +auto Mod::version() const -> QString +{ + return details().version; +} + +auto Mod::homepage() const -> QString +{ + QString metaUrl = Resource::homepage(); + + if (metaUrl.isEmpty()) + return details().homeurl; + else + return metaUrl; +} + +auto Mod::loaders() const -> QString +{ + if (metadata()) + { + QStringList loaders; + auto modLoaders = metadata()->loaders; + for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) + { + loaders << getModLoaderAsString(loader); + } + return loaders.join(", "); + } + + return {}; +} + +auto Mod::side() const -> QString +{ + if (metadata()) + return ModPlatform::SideUtils::toString(metadata()->side); + + return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); +} + +auto Mod::mcVersions() const -> QString +{ + if (metadata()) + return metadata()->mcVersions.join(", "); + + return {}; +} + +auto Mod::releaseType() const -> QString +{ + if (metadata()) + return metadata()->releaseType.toString(); + + return ModPlatform::IndexedVersionType().toString(); +} + +auto Mod::description() const -> QString +{ + return details().description; +} + +auto Mod::authors() const -> QStringList +{ + return details().authors; +} + +void Mod::finishResolvingWithDetails(ModDetails&& details) +{ + m_is_resolving = false; + m_is_resolved = true; + + m_local_details = std::move(details); + if (!iconPath().isEmpty()) + { + m_packImageCacheKey.wasReadAttempt = false; + } +} + +auto Mod::licenses() const -> const QList& +{ + return details().licenses; +} + +auto Mod::issueTracker() const -> QString +{ + return details().issue_tracker; +} + +QPixmap Mod::setIcon(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_packImageCacheKey.key.isValid()) + PixmapCache::remove(m_packImageCacheKey.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage( + new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_packImageCacheKey.key = PixmapCache::insert(pixmap); + m_packImageCacheKey.wasEverUsed = true; + m_packImageCacheKey.wasReadAttempt = true; + return pixmap; +} + +QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const +{ + auto pixmap_transform = [&size, &mode](QPixmap pixmap) + { + if (size.isNull()) + return pixmap; + return pixmap.scaled(size, mode, Qt::SmoothTransformation); + }; + + QPixmap cached_image; + if (PixmapCache::find(m_packImageCacheKey.key, &cached_image)) + { + return pixmap_transform(cached_image); + } + + // No valid image we can get + if ((!m_packImageCacheKey.wasEverUsed && m_packImageCacheKey.wasReadAttempt) || iconPath().isEmpty()) + return {}; + + if (m_packImageCacheKey.wasEverUsed) + { + qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. + m_packImageCacheKey.wasReadAttempt = true; + if (ModUtils::loadIconFile(*this, &cached_image)) + { + return pixmap_transform(cached_image); + } + // Image failed to load + return {}; +} + +bool Mod::valid() const +{ + return !m_local_details.mod_id.isEmpty(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/Mod.hpp b/archived/projt-launcher/launcher/minecraft/mod/Mod.hpp new file mode 100644 index 0000000000..f2756cac6a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Mod.hpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ModDetails.hpp" +#include "Resource.hpp" + +class Mod : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + using WeakPtr = QPointer; + + Mod() = default; + Mod(const QFileInfo& file); + Mod(QString file_path) : Mod(QFileInfo(file_path)) + {} + + auto details() const -> const ModDetails&; + auto name() const -> QString override; + auto mod_id() const -> QString; + auto version() const -> QString; + auto homepage() const -> QString override; + auto description() const -> QString; + auto authors() const -> QStringList; + auto licenses() const -> const QList&; + auto issueTracker() const -> QString; + auto side() const -> QString; + auto loaders() const -> QString; + auto mcVersions() const -> QString; + auto releaseType() const -> QString; + + /** Get the intneral path to the mod's icon file*/ + QString iconPath() const + { + return m_local_details.icon_file; + } + /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ + QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + /** Thread-safe. */ + QPixmap setIcon(QImage new_image) const; + + void setDetails(const ModDetails& details); + + bool valid() const override; + + [[nodiscard]] int compare(const Resource& other, SortType type) const override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + // Delete all the files of this mod + auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only + void destroyMetadata(QDir& index_dir); + + void finishResolvingWithDetails(ModDetails&& details); + + protected: + ModDetails m_local_details; + + mutable QMutex m_data_lock; + + struct + { + QPixmapCache::Key key; + bool wasEverUsed = false; + bool wasReadAttempt = false; + } mutable m_packImageCacheKey; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ModDetails.hpp b/archived/projt-launcher/launcher/minecraft/mod/ModDetails.hpp new file mode 100644 index 0000000000..a9900d6ee2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ModDetails.hpp @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include + +#include +#include +#include + +#include "minecraft/mod/MetadataHandler.hpp" + +struct ModLicense +{ + QString name = {}; + QString id = {}; + QString url = {}; + QString description = {}; + + ModLicense() + {} + + /** Parse a license string which may contain: + * - An SPDX identifier (e.g., "MIT", "GPL-3.0-only", "Apache-2.0") + * - A URL in parentheses or standalone + * - A plain text license name + * + * Supports SPDX license expressions: https://spdx.org/licenses/ + */ + ModLicense(const QString license) + { + if (license.isEmpty()) + { + return; + } + + QString remaining = license.trimmed(); + + // Common SPDX identifiers for quick matching + static const QStringList spdxIdentifiers = { "MIT", + "GPL-2.0", + "GPL-2.0-only", + "GPL-2.0-or-later", + "GPL-3.0", + "GPL-3.0-only", + "GPL-3.0-or-later", + "LGPL-2.0", + "LGPL-2.1", + "LGPL-3.0", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "MPL-2.0", + "ISC", + "Unlicense", + "WTFPL", + "Zlib", + "CC0-1.0", + "CC-BY-4.0", + "CC-BY-SA-4.0", + "AGPL-3.0", + "AGPL-3.0-only", + "AGPL-3.0-or-later", + "EPL-2.0", + "OSL-3.0", + "Artistic-2.0", + "BSL-1.0", + "All Rights Reserved", + "ARR", + "Custom", + "Proprietary" }; + + // SPDX to URL mapping for common licenses + static const QHash spdxUrls = { + { "MIT", "https://opensource.org/licenses/MIT" }, + { "GPL-2.0", "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { "GPL-2.0-only", "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { "GPL-2.0-or-later", "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { "GPL-3.0", "https://www.gnu.org/licenses/gpl-3.0.html" }, + { "GPL-3.0-only", "https://www.gnu.org/licenses/gpl-3.0.html" }, + { "GPL-3.0-or-later", "https://www.gnu.org/licenses/gpl-3.0.html" }, + { "LGPL-2.1", "https://www.gnu.org/licenses/lgpl-2.1.html" }, + { "LGPL-3.0", "https://www.gnu.org/licenses/lgpl-3.0.html" }, + { "Apache-2.0", "https://www.apache.org/licenses/LICENSE-2.0" }, + { "BSD-2-Clause", "https://opensource.org/licenses/BSD-2-Clause" }, + { "BSD-3-Clause", "https://opensource.org/licenses/BSD-3-Clause" }, + { "MPL-2.0", "https://www.mozilla.org/en-US/MPL/2.0/" }, + { "ISC", "https://opensource.org/licenses/ISC" }, + { "Unlicense", "https://unlicense.org/" }, + { "CC0-1.0", "https://creativecommons.org/publicdomain/zero/1.0/" }, + { "CC-BY-4.0", "https://creativecommons.org/licenses/by/4.0/" }, + { "CC-BY-SA-4.0", "https://creativecommons.org/licenses/by-sa/4.0/" }, + { "AGPL-3.0", "https://www.gnu.org/licenses/agpl-3.0.html" }, + { "AGPL-3.0-only", "https://www.gnu.org/licenses/agpl-3.0.html" }, + }; + + // Extract URL from parentheses or standalone + auto parts = remaining.split(' '); + QStringList urlParts; + + for (const auto& part : parts) + { + QString urlCandidate = part; + if (part.startsWith("(") && part.endsWith(")")) + { + urlCandidate = part.mid(1, part.size() - 2); + } + + QUrl parsedUrl(urlCandidate); + if (parsedUrl.isValid() && !parsedUrl.scheme().isEmpty() && !parsedUrl.host().isEmpty()) + { + this->url = parsedUrl.toString(); + urlParts.append(part); + } + } + + // Remove URL parts from remaining + for (const auto& urlPart : urlParts) + { + parts.removeOne(urlPart); + } + + QString licensePart = parts.join(' ').trimmed(); + + // Check if it's a known SPDX identifier + for (const QString& spdx : spdxIdentifiers) + { + if (licensePart.compare(spdx, Qt::CaseInsensitive) == 0) + { + this->id = spdx; + this->name = spdx; + this->description = spdx; + + // Set URL from SPDX mapping if not already set + if (this->url.isEmpty() && spdxUrls.contains(spdx)) + { + this->url = spdxUrls[spdx]; + } + return; + } + } + + // Not a known SPDX - treat as custom license + this->name = licensePart; + this->description = licensePart; + + if (parts.size() == 1) + { + this->id = parts.first(); + } + } + + ModLicense(const QString& name_, const QString& id_, const QString& url_, const QString& description_) + : name(name_), + id(id_), + url(url_), + description(description_) + {} + + ModLicense(const ModLicense& other) : name(other.name), id(other.id), url(other.url), description(other.description) + {} + + ModLicense& operator=(const ModLicense& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + ModLicense& operator=(const ModLicense&& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + bool isEmpty() + { + return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty(); + } +}; + +struct ModDetails +{ + /* Mod ID as defined in the ModLoader-specific metadata */ + QString mod_id = {}; + + /* Human-readable name */ + QString name = {}; + + /* Human-readable mod version */ + QString version = {}; + + /* Human-readable minecraft version */ + QString mcversion = {}; + + /* URL for mod's home page */ + QString homeurl = {}; + + /* Human-readable description */ + QString description = {}; + + /* List of the author's names */ + QStringList authors = {}; + + /* Issue Tracker URL */ + QString issue_tracker = {}; + + /* License */ + QList licenses = {}; + + /* Path of mod logo */ + QString icon_file = {}; + + ModDetails() = default; + + /** Metadata should be handled manually to properly set the mod status. */ + ModDetails(const ModDetails& other) + : mod_id(other.mod_id), + name(other.name), + version(other.version), + mcversion(other.mcversion), + homeurl(other.homeurl), + description(other.description), + authors(other.authors), + issue_tracker(other.issue_tracker), + licenses(other.licenses), + icon_file(other.icon_file) + {} + + ModDetails& operator=(const ModDetails& other) = default; + + ModDetails& operator=(ModDetails&& other) = default; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.cpp new file mode 100644 index 0000000000..e248059c40 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ModFolderModel.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "minecraft/mod/tasks/LocalModParseTask.hpp" + +ModFolderModel::ModFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", + "Image", + "Name", + "Version", + "Last Modified", + "Provider", + "Size", + "Side", + "Loaders", + "Minecraft Versions", + "Release Type" }); + m_column_names_translated = QStringList({ tr("Enable"), + tr("Image"), + tr("Name"), + tr("Version"), + tr("Last Modified"), + tr("Provider"), + tr("Size"), + tr("Side"), + tr("Loaders"), + tr("Minecraft Versions"), + tr("Release Type") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, + SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, + SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; +} + +QVariant ModFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + switch (column) + { + case VersionColumn: + { + switch (at(row).type()) + { + case ResourceType::FOLDER: return tr("Folder"); + case ResourceType::SINGLEFILE: return tr("File"); + default: return at(row).version(); + } + } + case SideColumn: return at(row).side(); + case LoadersColumn: return at(row).loaders(); + case McVersionsColumn: return at(row).mcVersions(); + case ReleaseTypeColumn: return at(row).releaseType(); + default: break; + } + break; + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case VersionColumn: + case DateColumn: + case ProviderColumn: + case ImageColumn: + case SideColumn: + case LoadersColumn: + case McVersionsColumn: + case ReleaseTypeColumn: + case SizeColumn: return columnNames().at(section); + default: return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: return tr("Is the mod enabled?"); + case NameColumn: return tr("The name of the mod."); + case VersionColumn: return tr("The version of the mod."); + case DateColumn: return tr("The date and time this mod was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the mod."); + case SideColumn: return tr("On what environment the mod is running."); + case LoadersColumn: return tr("The mod loader."); + case McVersionsColumn: return tr("The supported minecraft versions."); + case ReleaseTypeColumn: return tr("The release type."); + case SizeColumn: return tr("The size of the mod."); + default: return QVariant(); + } + default: return QVariant(); + } + return QVariant(); +} + +int ModFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Task* ModFolderModel::createParseTask(Resource& resource) +{ + return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); +} + +bool ModFolderModel::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd()) + return; + + int row = m_resources_index[mod_id]; + + auto parse_task = *iter; + auto cast_task = static_cast(parse_task.get()); + + Q_ASSERT(cast_task->token() == ticket); + + auto resource = find(mod_id); + + auto result = cast_task->result(); + if (result && resource) + static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); + + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.hpp new file mode 100644 index 0000000000..88f127704f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.hpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Mod.hpp" +#include "ResourceFolderModel.hpp" + +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class ModFolderModel : public ResourceFolderModel +{ + Q_OBJECT + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + VersionColumn, + DateColumn, + ProviderColumn, + SizeColumn, + SideColumn, + LoadersColumn, + McVersionsColumn, + ReleaseTypeColumn, + NUM_COLUMNS + }; + ModFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + virtual QString id() const override + { + return "mods"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override + { + return new Mod(file); + } + [[nodiscard]] Task* createParseTask(Resource&) override; + + bool isValid(); + + RESOURCE_HELPERS(Mod) + + private slots: + void onParseSucceeded(int ticket, QString resource_id) override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/Resource.cpp b/archived/projt-launcher/launcher/minecraft/mod/Resource.cpp new file mode 100644 index 0000000000..3f70a24f41 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Resource.cpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "Resource.hpp" + +#include +#include +#include +#include + +#include "FileSystem.h" +#include "StringUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +Resource::Resource(QObject* parent) : QObject(parent) +{} + +Resource::Resource(QFileInfo file_info) : QObject() +{ + setFile(file_info); +} + +void Resource::setFile(QFileInfo file_info) +{ + m_file_info = file_info; + parseFile(); +} + +static std::tuple calculateFileSize(const QFileInfo& file) +{ + if (file.isDir()) + { + auto dir = QDir(file.absoluteFilePath()); + dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + auto count = dir.count(); + auto str = QObject::tr("item"); + if (count != 1) + str = QObject::tr("items"); + return { QString("%1 %2").arg(QString::number(count), str), count }; + } + return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; +} + +void Resource::parseFile() +{ + QString file_name{ m_file_info.fileName() }; + + m_type = ResourceType::UNKNOWN; + + m_internal_id = file_name; + + std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); + if (m_file_info.isDir()) + { + m_type = ResourceType::FOLDER; + m_name = file_name; + } + else if (m_file_info.isFile()) + { + if (file_name.endsWith(".disabled")) + { + file_name.chop(9); + m_enabled = false; + } + + if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) + { + m_type = ResourceType::ZIPFILE; + file_name.chop(4); + } + else if (file_name.endsWith(".nilmod")) + { + m_type = ResourceType::ZIPFILE; + file_name.chop(7); + } + else if (file_name.endsWith(".litemod")) + { + m_type = ResourceType::LITEMOD; + file_name.chop(8); + } + else + { + m_type = ResourceType::SINGLEFILE; + } + + m_name = file_name; + } + + m_changed_date_time = m_file_info.lastModified(); +} + +auto Resource::name() const -> QString +{ + if (metadata()) + return metadata()->name; + + return m_name; +} + +static void removeThePrefix(QString& string) +{ + static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), + QRegularExpression::CaseInsensitiveOption); + string.remove(s_regex); + string = string.trimmed(); +} + +auto Resource::provider() const -> QString +{ + if (metadata()) + return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); + + return tr("Unknown"); +} + +auto Resource::homepage() const -> QString +{ + if (metadata()) + return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); + + return {}; +} + +void Resource::setMetadata(std::shared_ptr&& metadata) +{ + if (status() == ResourceStatus::NO_METADATA) + setStatus(ResourceStatus::INSTALLED); + + m_metadata = metadata; +} + +QStringList Resource::issues() const +{ + QStringList result; + result.reserve(m_issues.length()); + + for (const char* issue : m_issues) { + result.append(tr(issue)); + } + + return result; +} + +void Resource::updateIssues(const BaseInstance* inst) +{ + m_issues.clear(); + + if (m_metadata == nullptr) { + return; + } + + auto mcInst = dynamic_cast(inst); + if (mcInst == nullptr) { + return; + } + + auto profile = mcInst->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + + if (!m_metadata->mcVersions.empty() && !m_metadata->mcVersions.contains(mcVersion)) { + // delay translation until issues() is called + m_issues.append(QT_TR_NOOP("Not marked as compatible with the instance's game version.")); + } +} + +int Resource::compare(const Resource& other, SortType type) const +{ + switch (type) + { + default: + case SortType::ENABLED: + if (enabled() && !other.enabled()) + return 1; + if (!enabled() && other.enabled()) + return -1; + break; + case SortType::NAME: + { + QString this_name{ name() }; + QString other_name{ other.name() }; + + // Remove common prefixes like "The" for better alphabetical sorting + removeThePrefix(this_name); + removeThePrefix(other_name); + + return QString::compare(this_name, other_name, Qt::CaseInsensitive); + } + case SortType::DATE: + if (dateTimeChanged() > other.dateTimeChanged()) + return 1; + if (dateTimeChanged() < other.dateTimeChanged()) + return -1; + break; + case SortType::SIZE: + { + if (this->type() != other.type()) + { + if (this->type() == ResourceType::FOLDER) + return -1; + if (other.type() == ResourceType::FOLDER) + return 1; + } + + if (sizeInfo() > other.sizeInfo()) + return 1; + if (sizeInfo() < other.sizeInfo()) + return -1; + break; + } + case SortType::PROVIDER: + { + auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + } + + return 0; +} + +bool Resource::applyFilter(QRegularExpression filter) const +{ + return filter.match(name()).hasMatch(); +} + +bool Resource::enable(EnableAction action) +{ + if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) + return false; + + QString path = m_file_info.absoluteFilePath(); + QFile file(path); + + bool enable = true; + switch (action) + { + case EnableAction::ENABLE: enable = true; break; + case EnableAction::DISABLE: enable = false; break; + case EnableAction::TOGGLE: + default: enable = !enabled(); break; + } + + if (m_enabled == enable) + return false; + + if (enable) + { + // m_enabled is false, but there's no '.disabled' suffix. + if (!path.endsWith(".disabled")) + { + qWarning() << "Cannot enable resource" << name() << ": file does not have .disabled suffix"; + return false; + } + path.chop(9); + } + else + { + path += ".disabled"; + if (QFile::exists(path)) + { + path = FS::getUniqueResourceName(path); + } + } + if (!file.rename(path)) + return false; + + setFile(QFileInfo(path)); + + m_enabled = enable; + return true; +} + +auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool +{ + m_type = ResourceType::UNKNOWN; + + if (!preserve_metadata) + { + qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); + destroyMetadata(index_dir); + } + + return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); +} + +auto Resource::destroyMetadata(const QDir& index_dir) -> void +{ + if (metadata()) + { + Metadata::remove(index_dir, metadata()->slug); + } + else + { + auto n = name(); + Metadata::remove(index_dir, n); + } + m_metadata = nullptr; +} + +bool Resource::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool Resource::isMoreThanOneHardLink() const +{ + return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1; +} + +auto Resource::getOriginalFileName() const -> QString +{ + auto fileName = m_file_info.fileName(); + if (!m_enabled) + fileName.chop(9); + return fileName; +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/mod/Resource.hpp b/archived/projt-launcher/launcher/minecraft/mod/Resource.hpp new file mode 100644 index 0000000000..a95ce79dd6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Resource.hpp @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include + +#include "MetadataHandler.hpp" +#include "QObjectPtr.h" + +class BaseInstance; + +enum class ResourceType +{ + UNKNOWN, //!< Indicates an unspecified resource type. + ZIPFILE, //!< The resource is a zip file containing the resource's class files. + SINGLEFILE, //!< The resource is a single file (not a zip file). + FOLDER, //!< The resource is in a folder on the filesystem. + LITEMOD, //!< The resource is a litemod +}; + +enum class ResourceStatus +{ + INSTALLED, // Both JAR and Metadata are present + NOT_INSTALLED, // Only the Metadata is present + NO_METADATA, // Only the JAR is present + UNKNOWN, // Default status +}; + +enum class SortType +{ + NAME, + DATE, + VERSION, + ENABLED, + PACK_FORMAT, + PROVIDER, + SIZE, + SIDE, + MC_VERSIONS, + LOADERS, + RELEASE_TYPE +}; + +enum class EnableAction +{ + ENABLE, + DISABLE, + TOGGLE +}; + +/** General class for managed resources. It mirrors a file in disk, with some more info + * for display and house-keeping purposes. + * + * Subclass it to add additional data / behavior, such as Mods or Resource packs. + */ +class Resource : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(Resource) + public: + using Ptr = shared_qobject_ptr; + using WeakPtr = QPointer; + + Resource(QObject* parent = nullptr); + Resource(QFileInfo file_info); + Resource(QString file_path) : Resource(QFileInfo(file_path)) + {} + + ~Resource() override = default; + + void setFile(QFileInfo file_info); + void parseFile(); + + auto fileinfo() const -> QFileInfo + { + return m_file_info; + } + auto dateTimeChanged() const -> QDateTime + { + return m_changed_date_time; + } + auto internal_id() const -> QString + { + return m_internal_id; + } + auto type() const -> ResourceType + { + return m_type; + } + bool enabled() const + { + return m_enabled; + } + auto getOriginalFileName() const -> QString; + QString sizeStr() const + { + return m_size_str; + } + qint64 sizeInfo() const + { + return m_size_info; + } + + virtual auto name() const -> QString; + virtual bool valid() const + { + return m_type != ResourceType::UNKNOWN; + } + + auto status() const -> ResourceStatus + { + return m_status; + }; + auto metadata() -> std::shared_ptr + { + return m_metadata; + } + auto metadata() const -> std::shared_ptr + { + return m_metadata; + } + auto provider() const -> QString; + virtual auto homepage() const -> QString; + + void setStatus(ResourceStatus status) + { + m_status = status; + } + void setMetadata(std::shared_ptr&& metadata); + void setMetadata(const Metadata::ModStruct& metadata) + { + setMetadata(std::make_shared(metadata)); + } + + QStringList issues() const; + void updateIssues(const BaseInstance* inst); + bool hasIssues() const + { + return !m_issues.empty(); + } + + /** Compares two Resources, for sorting purposes, considering a ascending order, returning: + * > 0: 'this' comes after 'other' + * = 0: 'this' is equal to 'other' + * < 0: 'this' comes before 'other' + */ + virtual int compare(Resource const& other, SortType type = SortType::NAME) const; + + /** Returns whether the given filter should filter out 'this' (false), + * or if such filter includes the Resource (true). + */ + virtual bool applyFilter(QRegularExpression filter) const; + + /** Changes the enabled property, according to 'action'. + * + * Returns whether a change was applied to the Resource's properties. + */ + bool enable(EnableAction action); + + auto shouldResolve() const -> bool + { + return !m_is_resolving && !m_is_resolved; + } + auto isResolving() const -> bool + { + return m_is_resolving; + } + auto isResolved() const -> bool + { + return m_is_resolved; + } + auto resolutionTicket() const -> int + { + return m_resolution_ticket; + } + + void setResolving(bool resolving, int resolutionTicket) + { + m_is_resolving = resolving; + m_resolution_ticket = resolutionTicket; + } + + // Delete all files of this resource. + auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only. + auto destroyMetadata(const QDir& index_dir) -> void; + + auto isSymLink() const -> bool + { + return m_file_info.isSymLink(); + } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in + * that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + bool isSymLinkUnder(const QString& instPath) const; + + bool isMoreThanOneHardLink() const; + + auto mod_id() const -> QString + { + return m_mod_id; + } + void setModId(const QString& modId) + { + m_mod_id = modId; + } + + protected: + /* The file corresponding to this resource. */ + QFileInfo m_file_info; + /* The cached date when this file was last changed. */ + QDateTime m_changed_date_time; + + /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */ + QString m_internal_id; + /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ + QString m_name; + QString m_mod_id; + + /* The type of file we're dealing with. */ + ResourceType m_type = ResourceType::UNKNOWN; + + /* Installation status of the resource. */ + ResourceStatus m_status = ResourceStatus::UNKNOWN; + + std::shared_ptr m_metadata = nullptr; + + /* Whether the resource is enabled (e.g. shows up in the game) or not. */ + bool m_enabled = true; + QList m_issues; + + /* Used to keep trach of pending / concluded actions on the resource. */ + bool m_is_resolving = false; + bool m_is_resolved = false; + int m_resolution_ticket = 0; + QString m_size_str; + qint64 m_size_info; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.cpp new file mode 100644 index 0000000000..93cffb6da2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -0,0 +1,1094 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ResourceFolderModel.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "FileSystem.h" + +#include "minecraft/mod/tasks/ResourceFolderLoadTask.hpp" + +#include "Json.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.hpp" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "settings/Setting.h" +#include "tasks/Task.h" +#include "ui/dialogs/CustomMessageBox.h" + +ResourceFolderModel::ResourceFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : QAbstractListModel(parent), + m_dir(dir), + m_instance(instance), + m_watcher(this), + m_is_indexed(is_indexed) +{ + if (create_dir) + { + FS::ensureFolderPathExists(m_dir.absolutePath()); + } + + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); + connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); }); + if (APPLICATION_DYN) + { // in tests the application macro doesn't work + m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + } +} + +ResourceFolderModel::~ResourceFolderModel() +{ + while (!QThreadPool::globalInstance()->waitForDone(100)) + QCoreApplication::processEvents(); +} + +bool ResourceFolderModel::startWatching(const QStringList& paths) +{ + // Remove orphaned metadata next time + m_first_folder_load = true; + + if (m_is_watching) + return false; + + auto couldnt_be_watched = m_watcher.addPaths(paths); + for (auto path : paths) + { + if (couldnt_be_watched.contains(path)) + qDebug() << "Failed to start watching " << path; + else + qDebug() << "Started watching " << path; + } + + update(); + + m_is_watching = !m_is_watching; + return m_is_watching; +} + +bool ResourceFolderModel::stopWatching(const QStringList& paths) +{ + if (!m_is_watching) + return false; + + auto couldnt_be_stopped = m_watcher.removePaths(paths); + for (auto path : paths) + { + if (couldnt_be_stopped.contains(path)) + qDebug() << "Failed to stop watching " << path; + else + qDebug() << "Stopped watching " << path; + } + + m_is_watching = !m_is_watching; + return !m_is_watching; +} + +bool ResourceFolderModel::installResource(QString original_path) +{ + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + original_path = FS::NormalizePath(original_path); + QFileInfo file_info(original_path); + + if (!file_info.exists() || !file_info.isReadable()) + { + qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; + return false; + } + qDebug() << "Installing: " << file_info.absoluteFilePath(); + + Resource resource(file_info); + if (!resource.valid()) + { + qWarning() << original_path << "is not a valid resource. Ignoring it."; + return false; + } + + auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); + if (original_path == new_path) + { + qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; + return false; + } + + auto triggerUpdate = [this]() + { + // Always schedule an update so callers relying on updateFinished (like tests) don't hang when watching is on. + auto started = update(); + return m_is_watching ? true : started; + }; + + switch (resource.type()) + { + case ResourceType::SINGLEFILE: + case ResourceType::ZIPFILE: + case ResourceType::LITEMOD: + { + if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) + { + if (!FS::deletePath(new_path)) + { + qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; + return false; + } + qDebug() << new_path << "has been deleted."; + } + + if (!QFile::copy(original_path, new_path)) + { + qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; + return false; + } + + FS::updateTimestamp(new_path); + + QFileInfo new_path_file_info(new_path); + resource.setFile(new_path_file_info); + + return triggerUpdate(); + } + case ResourceType::FOLDER: + { + if (QFile::exists(new_path)) + { + qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; + return false; + } + + if (!FS::copy(original_path, new_path)()) + { + qWarning() << "Copy of folder from" << original_path << "to" << new_path + << "has (potentially partially) failed."; + return false; + } + + QFileInfo newpathInfo(new_path); + resource.setFile(newpathInfo); + + return triggerUpdate(); + } + default: break; + } + return false; +} + +void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) +{ + auto install = [this, path] { installResource(std::move(path)); }; + if (vers.addonId.isValid()) + { + ModPlatform::IndexedPack pack{ + vers.addonId, + ModPlatform::ResourceProvider::FLAME, + }; + + auto response = std::make_shared(); + auto job = FlameAPI().getProject(vers.addonId.toString(), response); + connect(job.get(), &Task::failed, this, install); + connect(job.get(), &Task::aborted, this, install); + connect(job.get(), + &Task::succeeded, + [response, this, &vers, install, &pack] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *response; + return; + } + try + { + auto obj = Json::requireObject(Json::requireObject(doc), "data"); + FlameMod::loadIndexedPack(pack, obj); + } + catch (const JSONValidationError& e) + { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); + connect(&update_metadata, &Task::finished, this, install); + update_metadata.start(); + }); + + job->start(); + } + else + { + install(); + } +} + +bool ResourceFolderModel::uninstallResource(QString file_name, bool preserve_metadata) +{ + for (auto& resource : m_resources) + { + if (resource->fileinfo().fileName() == file_name) + { + auto res = resource->destroy(indexDir(), preserve_metadata, false); + + update(); + + return res; + } + } + return false; +} + +bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return true; + + for (auto i : indexes) + { + if (i.column() != 0) + continue; + + auto& resource = m_resources.at(i.row()); + resource->destroy(indexDir()); + } + + update(); + + return true; +} + +void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return; + + for (auto i : indexes) + { + if (i.column() != 0) + continue; + + auto& resource = m_resources.at(i.row()); + resource->destroyMetadata(indexDir()); + } + + update(); +} + +bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) +{ + if (m_instance != nullptr && m_instance->isRunning()) + { + auto response = CustomMessageBox::selectable( + nullptr, + tr("Confirm toggle"), + tr("If you enable/disable this resource while the game is running it may crash your game.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return false; + } + + if (indexes.isEmpty()) + return true; + + bool succeeded = true; + for (auto const& idx : indexes) + { + if (!validateIndex(idx) || idx.column() != 0) + continue; + + int row = idx.row(); + + auto& resource = m_resources[row]; + + // Preserve the row, but change its ID + auto old_id = resource->internal_id(); + if (!resource->enable(action)) + { + succeeded = false; + continue; + } + + auto new_id = resource->internal_id(); + + m_resources_index.remove(old_id); + m_resources_index[new_id] = row; + + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + + return succeeded; +} + +static QMutex s_update_task_mutex; +bool ResourceFolderModel::update() +{ + // We hold a lock here to prevent race conditions on the m_current_update_task reset. + QMutexLocker lock(&s_update_task_mutex); + + // Already updating, so we schedule a future update and return. + if (m_current_update_task) + { + m_scheduled_update = true; + return false; + } + + m_current_update_task.reset(createUpdateTask()); + if (!m_current_update_task) + return false; + + connect(m_current_update_task.get(), + &Task::succeeded, + this, + &ResourceFolderModel::onUpdateSucceeded, + Qt::ConnectionType::QueuedConnection); + connect(m_current_update_task.get(), + &Task::failed, + this, + &ResourceFolderModel::onUpdateFailed, + Qt::ConnectionType::QueuedConnection); + connect( + m_current_update_task.get(), + &Task::finished, + this, + [this] + { + m_current_update_task.reset(); + if (m_scheduled_update) + { + m_scheduled_update = false; + update(); + } + else + { + emit updateFinished(); + } + }, + Qt::ConnectionType::QueuedConnection); + + QThreadPool::globalInstance()->start(m_current_update_task.get()); + + return true; +} + +void ResourceFolderModel::resolveResource(Resource::Ptr res) +{ + if (!res->shouldResolve()) + { + return; + } + + Task::Ptr task{ createParseTask(*res) }; + if (!task) + return; + + int ticket = m_next_resolution_ticket.fetch_add(1); + + res->setResolving(true, ticket); + m_active_parse_tasks.insert(ticket, task); + + connect( + task.get(), + &Task::succeeded, + this, + [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); + connect( + task.get(), + &Task::failed, + this, + [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); + connect( + task.get(), + &Task::finished, + this, + [this, ticket] + { + m_active_parse_tasks.remove(ticket); + emit parseFinished(); + }, + Qt::ConnectionType::QueuedConnection); + + m_helper_thread_task.addTask(task); + + if (!m_helper_thread_task.isRunning()) + { + QThreadPool::globalInstance()->start(&m_helper_thread_task); + } +} + +void ResourceFolderModel::onUpdateSucceeded() +{ + auto update_results = static_cast(m_current_update_task.get())->result(); + + auto& new_resources = update_results->resources; + + auto current_list = m_resources_index.keys(); + QSet current_set(current_list.begin(), current_list.end()); + + auto new_list = new_resources.keys(); + QSet new_set(new_list.begin(), new_list.end()); + + applyUpdates(current_set, new_set, new_resources); +} + +void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) + return; + + int row = m_resources_index[resource_id]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} + +Task* ResourceFolderModel::createUpdateTask() +{ + auto index_dir = indexDir(); + auto task = new ResourceFolderLoadTask(dir(), + index_dir, + m_is_indexed, + m_first_folder_load, + [this](const QFileInfo& file) { return createResource(file); }); + m_first_folder_load = false; + return task; +} + +bool ResourceFolderModel::hasPendingParseTasks() const +{ + return !m_active_parse_tasks.isEmpty(); +} + +void ResourceFolderModel::directoryChanged(QString path) +{ + update(); +} + +Qt::DropActions ResourceFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other resource lists + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + auto flags = defaultFlags | Qt::ItemIsDropEnabled; + if (index.isValid()) + flags |= Qt::ItemIsUserCheckable; + return flags; +} + +QStringList ResourceFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +{ + if (action == Qt::IgnoreAction) + { + return true; + } + + // check if the action is supported + if (!data || !(action & supportedDropActions())) + { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + { + continue; + } + + QString sourcePath = url.toLocalFile(); + + // Handle move vs copy action + if (action == Qt::MoveAction) + { + // Move: install then delete source + if (installResource(sourcePath)) + { + QFile::remove(sourcePath); + } + else + { + qWarning() << "Failed to move resource from" << sourcePath; + } + } + else + { + // Copy: just install (copies the file) + if (!installResource(sourcePath)) + { + qWarning() << "Failed to install resource from" << sourcePath; + } + } + } + return true; + } + return false; +} + +bool ResourceFolderModel::validateIndex(const QModelIndex& index) const +{ + if (!index.isValid()) + return false; + + int row = index.row(); + if (row < 0 || row >= m_resources.size()) + return false; + + return true; +} + +QBrush ResourceFolderModel::rowBackground(int row) const +{ + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) + { + return { QColor(255, 0, 0, 40) }; + } + return {}; +} + +QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + switch (column) + { + case NameColumn: return m_resources[row]->name(); + case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: return m_resources[row]->provider(); + case SizeColumn: return m_resources[row]->sizeStr(); + default: return {}; + } + case Qt::ToolTipRole: + { + QString tooltip = m_resources[row]->internal_id(); + if (column == NameColumn) + { + if (APPLICATION->settings()->get("ShowModIncompat").toBool()) + { + for (const QString& issue : at(row).issues()) + { + tooltip += "\n" + issue; + } + } + if (at(row).isSymLinkUnder(instDirPath())) + { + tooltip += tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also " + "change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath()); + } + if (at(row).isMoreThanOneHardLink()) + { + tooltip += tr( + "\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + return tooltip; + } + case Qt::DecorationRole: + { + if (column == NameColumn) + { + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && at(row).hasIssues()) + return QIcon::fromTheme("status-bad"); + if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) + return QIcon::fromTheme("status-yellow"); + } + + return {}; + } + case Qt::CheckStateRole: + if (column == ActiveColumn) + return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + return {}; + default: return {}; + } +} + +bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) +{ + int row = index.row(); + if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) + return false; + + if (role == Qt::CheckStateRole) + { + return setResourceEnabled({ index }, EnableAction::TOGGLE); + } + + return false; +} + +QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case DateColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); + default: return {}; + } + case Qt::ToolTipRole: + { + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + switch (section) + { + case ActiveColumn: return tr("Is the resource enabled?"); + case NameColumn: return tr("The name of the resource."); + case DateColumn: return tr("The date and time this resource was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the resource."); + case SizeColumn: return tr("The size of the resource."); + default: return {}; + } + } + default: break; + } + + return {}; +} + +void ResourceFolderModel::setupHeaderAction(QAction* act, int column) +{ + Q_ASSERT(act); + + act->setText(columnNames().at(column)); +} + +void ResourceFolderModel::saveColumns(QTreeView* tree) +{ + auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); + + auto stateSetting = m_instance->settings()->getSetting(stateSettingName); + stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); + + // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate + // is false + auto settings = m_instance->settings(); + if (!settings->get(overrideSettingName).toBool()) + { + settings = APPLICATION->settings(); + } + auto visibility = Json::toMap(settings->get(visibilitySettingName).toString()); + for (auto i = 0; i < m_column_names.size(); ++i) + { + if (m_columnsHideable[i]) + { + auto name = m_column_names[i]; + visibility[name] = !tree->isColumnHidden(i); + } + } + settings->set(visibilitySettingName, Json::fromMap(visibility)); +} + +void ResourceFolderModel::loadColumns(QTreeView* tree) +{ + auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); + + auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); + tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); + + auto setVisible = [this, tree](QVariant value) + { + auto visibility = Json::toMap(value.toString()); + for (auto i = 0; i < m_column_names.size(); ++i) + { + if (m_columnsHideable[i]) + { + auto name = m_column_names[i]; + tree->setColumnHidden(i, !visibility.value(name, false).toBool()); + } + } + }; + + auto const defaultValue = Json::fromMap({ + { "Image", true }, + { "Version", true }, + { "Last Modified", true }, + { "Provider", true }, + { "Pack Format", true }, + }); + // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate + // is false + auto settings = m_instance->settings(); + if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) + { + settings = APPLICATION->settings(); + } + auto visibility = settings->getOrRegisterSetting(visibilitySettingName, defaultValue); + setVisible(visibility->get()); + + // allways connect the signal in case the setting is toggled on and off + auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue); + connect(gSetting.get(), + &Setting::SettingChanged, + tree, + [this, setVisible, overrideSettingName](const Setting&, QVariant value) + { + if (!m_instance->settings()->get(overrideSettingName).toBool()) + { + setVisible(value); + } + }); +} + +QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) +{ + auto menu = new QMenu(tree); + + { // action to decide if the visibility is per instance or not + auto act = new QAction(tr("Override Columns Visibility"), menu); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + + act->setCheckable(true); + act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); + + connect(act, + &QAction::toggled, + tree, + [this, tree, overrideSettingName](bool toggled) + { + m_instance->settings()->set(overrideSettingName, toggled); + saveColumns(tree); + }); + + menu->addAction(act); + } + menu->addSeparator()->setText(tr("Show / Hide Columns")); + + for (int col = 0; col < columnCount(); ++col) + { + // Skip creating actions for columns that should not be hidden + if (!m_columnsHideable.at(col)) + continue; + auto act = new QAction(menu); + setupHeaderAction(act, col); + + act->setCheckable(true); + act->setChecked(!tree->isColumnHidden(col)); + + connect(act, + &QAction::toggled, + tree, + [this, col, tree](bool toggled) + { + tree->setColumnHidden(col, !toggled); + for (int c = 0; c < columnCount(); ++c) + { + if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) + tree->resizeColumnToContents(c); + } + saveColumns(tree); + }); + + menu->addAction(act); + } + + return menu; +} + +QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent) +{ + return new ProxyModel(parent); +} + +SortType ResourceFolderModel::columnToSortKey(size_t column) const +{ + Q_ASSERT(m_column_sort_keys.size() == columnCount()); + return m_column_sort_keys.at(column); +} + +/* Standard Proxy Model for createFilterProxyModel */ +bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, + [[maybe_unused]] const QModelIndex& source_parent) const +{ + auto* model = qobject_cast(sourceModel()); + if (!model) + return true; + + const auto& resource = model->at(source_row); + + return resource.applyFilter(filterRegularExpression()); +} + +bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const +{ + auto* model = qobject_cast(sourceModel()); + if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) + { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + + // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants + // unconditionally and proceed. + + auto column_sort_key = model->columnToSortKey(source_left.column()); + auto const& resource_left = model->at(source_left.row()); + auto const& resource_right = model->at(source_right.row()); + + auto compare_result = resource_left.compare(resource_right, column_sort_key); + if (compare_result == 0) + return QSortFilterProxyModel::lessThan(source_left, source_right); + + return compare_result < 0; +} + +QString ResourceFolderModel::instDirPath() const +{ + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} + +void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) + return; + + auto removed_index = m_resources_index[resource_id]; + auto removed_it = m_resources.begin() + removed_index; + Q_ASSERT(removed_it != m_resources.end()); + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + + // update index + m_resources_index.clear(); + int idx = 0; + for (auto const& mod : qAsConst(m_resources)) + { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + endRemoveRows(); +} + +void ResourceFolderModel::applyUpdates(QSet& current_set, + QSet& new_set, + QMap& new_resources) +{ + // see if the kept resources changed in some way + { + QSet kept_set = current_set; + kept_set.intersect(new_set); + + for (auto const& kept : kept_set) + { + auto row_it = m_resources_index.constFind(kept); + Q_ASSERT(row_it != m_resources_index.constEnd()); + auto row = row_it.value(); + + auto& new_resource = new_resources[kept]; + auto const& current_resource = m_resources.at(row); + + if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) + { + bool hadIssues = current_resource->hasIssues(); + current_resource->updateIssues(m_instance); + if (hadIssues != current_resource->hasIssues()) + { + emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); + } + continue; + } + + // If the resource is resolving, but something about it changed, we don't want to + // continue the resolving. + if (current_resource->isResolving()) + { + auto ticket = current_resource->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) + { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + m_resources[row].reset(new_resource); + new_resource->updateIssues(m_instance); + resolveResource(m_resources.at(row)); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove resources no longer present + { + QSet removed_set = current_set; + removed_set.subtract(new_set); + + QList removed_rows; + for (auto& removed : removed_set) + removed_rows.append(m_resources_index[removed]); + + std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); + + for (auto& removed_index : removed_rows) + { + auto removed_it = m_resources.begin() + removed_index; + + Q_ASSERT(removed_it != m_resources.end()); + + if ((*removed_it)->isResolving()) + { + auto ticket = (*removed_it)->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) + { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + endRemoveRows(); + } + } + + // add new resources to the end + { + QSet added_set = new_set; + added_set.subtract(current_set); + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (added_set.size() > 0) + { + beginInsertRows(QModelIndex(), + static_cast(m_resources.size()), + static_cast(m_resources.size() + added_set.size() - 1)); + + for (auto& added : added_set) + { + auto res = new_resources[added]; + res->updateIssues(m_instance); + m_resources.append(res); + resolveResource(m_resources.last()); + } + + endInsertRows(); + } + } + + // update index + { + m_resources_index.clear(); + int idx = 0; + for (auto const& mod : qAsConst(m_resources)) + { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + } +} +Resource::Ptr ResourceFolderModel::find(QString id) +{ + auto iter = std::find_if(m_resources.constBegin(), + m_resources.constEnd(), + [&](Resource::Ptr const& r) { return r->internal_id() == id; }); + if (iter == m_resources.constEnd()) + return nullptr; + return *iter; +} + +Resource::WeakPtr ResourceFolderModel::findWeak(const QString& id) +{ + auto it = m_resources_index.constFind(id); + if (it == m_resources_index.constEnd()) + return Resource::WeakPtr(); + + int idx = it.value(); + if (idx < 0 || idx >= m_resources.size()) + return Resource::WeakPtr(); + + return Resource::WeakPtr(m_resources[idx].get()); +} + +QList ResourceFolderModel::allResources() +{ + QList result; + result.reserve(m_resources.size()); + for (const Resource ::Ptr& resource : m_resources) + result.append((resource.get())); + return result; +} +QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) +{ + QList result; + for (const QModelIndex& index : indexes) + { + if (index.column() != 0) + continue; + result.append(&at(index.row())); + } + return result; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.hpp new file mode 100644 index 0000000000..e909f0d26b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.hpp @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Resource.hpp" + +#include "BaseInstance.h" + +#include "tasks/ConcurrentTask.h" +#include "tasks/Task.h" + +class QSortFilterProxyModel; + +/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ +#define RESOURCE_HELPERS(T) \ + T& at(int index) \ + { \ + return *static_cast(m_resources[index].get()); \ + } \ + const T& at(int index) const \ + { \ + return *static_cast(m_resources.at(index).get()); \ + } \ + QList selected##T##s(const QModelIndexList& indexes) \ + { \ + QList result; \ + for (const QModelIndex& index : indexes) \ + { \ + if (index.column() != 0) \ + continue; \ + \ + result.append(&at(index.row())); \ + } \ + return result; \ + } \ + QList all##T##s() \ + { \ + QList result; \ + result.reserve(m_resources.size()); \ + \ + for (const Resource::Ptr& resource : m_resources) \ + result.append(static_cast(resource.get())); \ + \ + return result; \ + } + +/** A basic model for external resources. + * + * This model manages a list of resources. As such, external users of such resources do not own them, + * and the resource's lifetime is contingent on the model's lifetime. + * + * Weak pointer access is provided via weakAt(), findWeak(), and isResourceValid() methods. + * External code should use these to avoid extending resource lifetime. + */ +class ResourceFolderModel : public QAbstractListModel +{ + Q_OBJECT + public: + ResourceFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + ~ResourceFolderModel() override; + + virtual QString id() const + { + return "resource"; + } + + /** Starts watching the paths for changes. + * + * Returns whether starting to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool startWatching(const QStringList& paths); + + /** Stops watching the paths for changes. + * + * Returns whether stopping to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool stopWatching(const QStringList& paths); + + /* Helper methods for subclasses, using a predetermined list of paths. */ + virtual bool startWatching() + { + return startWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); + } + virtual bool stopWatching() + { + return stopWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); + } + + QDir indexDir() + { + return { QString("%1/.index").arg(dir().absolutePath()) }; + } + + /** Given a path in the system, install that resource, moving it to its place in the + * instance file hierarchy. + * + * Returns whether the installation was succcessful. + */ + virtual bool installResource(QString path); + + virtual void installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); + + /** Uninstall (i.e. remove all data about it) a resource, given its file name. + * + * Returns whether the removal was successful. + */ + virtual bool uninstallResource(QString file_name, bool preserve_metadata = false); + virtual bool deleteResources(const QModelIndexList&); + virtual void deleteMetadata(const QModelIndexList&); + + /** Applies the given 'action' to the resources in 'indexes'. + * + * Returns whether the action was successfully applied to all resources. + */ + virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action); + + /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ + virtual bool update(); + + /** Creates a new parse task, if needed, for 'res' and start it.*/ + virtual void resolveResource(Resource::Ptr res); + + qsizetype size() const + { + return m_resources.size(); + } + [[nodiscard]] bool empty() const + { + return size() == 0; + } + + Resource& at(int index) + { + return *m_resources[index].get(); + } + const Resource& at(int index) const + { + return *m_resources.at(index).get(); + } + + /** Get a weak pointer to a resource by index. + * This is the preferred way to access resources from external code, + * as it doesn't extend the resource's lifetime. + */ + Resource::WeakPtr weakAt(int index) + { + if (index < 0 || index >= m_resources.size()) + return Resource::WeakPtr(); + return Resource::WeakPtr(m_resources[index].get()); + } + + /** Get a weak pointer to a resource by internal ID. + * Returns null WeakPtr if not found. + */ + Resource::WeakPtr findWeak(const QString& id); + + /** Check if a weak pointer is still valid (resource still exists). + * This is useful for external code that holds weak references. + */ + bool isResourceValid(Resource::WeakPtr ptr) const + { + return !ptr.isNull() && m_resources_index.contains(ptr->internal_id()); + } + + QList selectedResources(const QModelIndexList& indexes); + QList allResources(); + + Resource::Ptr find(QString id); + + QDir const& dir() const + { + return m_dir; + } + + /** Checks whether there's any parse tasks being done. + * + * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model + * while having such tasks would introduce an undefined behavior, most likely resulting in a crash. + */ + bool hasPendingParseTasks() const; + + /* Qt behavior */ + + /* Basic columns */ + enum Columns + { + ActiveColumn = 0, + NameColumn, + DateColumn, + ProviderColumn, + SizeColumn, + NUM_COLUMNS + }; + + QStringList columnNames(bool translated = true) const + { + return translated ? m_column_names_translated : m_column_names; + } + + int rowCount(const QModelIndex& parent = {}) const override + { + return parent.isValid() ? 0 : static_cast(size()); + } + int columnCount(const QModelIndex& parent = {}) const override + { + return parent.isValid() ? 0 : NUM_COLUMNS; + } + + Qt::DropActions supportedDropActions() const override; + + /// flags, mostly to support drag&drop + Qt::ItemFlags flags(const QModelIndex& index) const override; + QStringList mimeTypes() const override; + [[nodiscard]] bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) override; + + [[nodiscard]] bool validateIndex(const QModelIndex& index) const; + + QBrush rowBackground(int row) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void setupHeaderAction(QAction* act, int column); + void saveColumns(QTreeView* tree); + void loadColumns(QTreeView* tree); + QMenu* createHeaderContextMenu(QTreeView* tree); + + /** This creates a proxy model to filter / sort the model for a UI. + * + * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! + */ + QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); + + SortType columnToSortKey(size_t column) const; + QList columnResizeModes() const + { + return m_column_resize_modes; + } + + class ProxyModel : public QSortFilterProxyModel + { + public: + explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) + {} + + protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; + }; + + QString instDirPath() const; + + signals: + void updateFinished(); + void parseFinished(); + + protected: + /** This creates a new update task to be executed by update(). + * + * The task should load and parse all resources necessary, and provide a way of accessing such results. + * + * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. + * If such work is needed, try using it in the Task create by createParseTask() instead! + */ + [[nodiscard]] Task* createUpdateTask(); + + [[nodiscard]] virtual Resource* createResource(const QFileInfo& info) + { + return new Resource(info); + } + + /** This creates a new parse task to be executed by onUpdateSucceeded(). + * + * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets + * executed in the background, so it slowly updates the UI as tasks get done. + */ + [[nodiscard]] virtual Task* createParseTask(Resource&) + { + return nullptr; + } + + /** Standard implementation of the model update logic. + * + * It uses set operations to find differences between the current state and the updated state, + * to act only on those disparities. + * + */ + void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); + + protected slots: + void directoryChanged(QString); + + /** Called when the update task is successful. + * + * Override in subclasses to handle specific task result types. + * The implementation typically uses static_cast to convert the Task to + * the specific type returned by createUpdateTask(). + * + * Note: Qt's Q_OBJECT macro doesn't support template classes, so we use + * runtime polymorphism with virtual methods and static_cast instead. + * The type relationship is documented in createUpdateTask() for each subclass. + */ + virtual void onUpdateSucceeded(); + virtual void onUpdateFailed() + {} + + /** Called when the parse task with the given ticket is successful. + * + * This is just a simple reference implementation. You probably want to override it with your own logic in a + * subclass if the resource is complex and has more stuff to parse. + */ + virtual void onParseSucceeded(int ticket, QString resource_id); + virtual void onParseFailed(int ticket, QString resource_id); + + protected: + // Represents the relationship between a column's index (represented by the list index), and it's sorting key. + // As such, the order in with they appear is very important! + QList m_column_sort_keys = { SortType::ENABLED, + SortType::NAME, + SortType::DATE, + SortType::PROVIDER, + SortType::SIZE }; + QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; + QStringList m_column_names_translated = { tr("Enable"), + tr("Name"), + tr("Last Modified"), + tr("Provider"), + tr("Size") }; + QList m_column_resize_modes = { QHeaderView::Interactive, + QHeaderView::Stretch, + QHeaderView::Interactive, + QHeaderView::Interactive, + QHeaderView::Interactive }; + QList m_columnsHideable = { false, false, true, true, true }; + + QDir m_dir; + BaseInstance* m_instance; + QFileSystemWatcher m_watcher; + bool m_is_watching = false; + + bool m_is_indexed; + bool m_first_folder_load = true; + + Task::Ptr m_current_update_task = nullptr; + bool m_scheduled_update = false; + + QList m_resources; + + // Represents the relationship between a resource's internal ID and it's row position on the model. + QMap m_resources_index; + + ConcurrentTask m_helper_thread_task; + QMap m_active_parse_tasks; + std::atomic m_next_resolution_ticket = 0; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.cpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.cpp new file mode 100644 index 0000000000..e0de7aff25 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ResourcePack.hpp" + +#include +#include +#include +#include "MTPixmapCache.h" +#include "Version.h" + +// Values taken from: +// https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats +static const QMap> s_pack_format_versions = { + { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, + { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, + { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } }, + { 14, { Version("23w14a"), Version("23w16a") } }, { 15, { Version("1.20"), Version("1.20.1") } }, + { 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } }, + { 18, { Version("1.20.2"), Version("23w16a") } }, { 19, { Version("23w42a"), Version("23w42a") } }, + { 20, { Version("23w43a"), Version("23w44a") } }, { 21, { Version("23w45a"), Version("23w46a") } }, + { 22, { Version("1.20.3-pre1"), Version("23w51b") } }, { 24, { Version("24w03a"), Version("24w04a") } }, + { 25, { Version("24w05a"), Version("24w05b") } }, { 26, { Version("24w06a"), Version("24w07a") } }, + { 28, { Version("24w09a"), Version("24w10a") } }, { 29, { Version("24w11a"), Version("24w11a") } }, + { 30, { Version("24w12a"), Version("23w12a") } }, { 31, { Version("24w13a"), Version("1.20.5-pre3") } }, + { 32, { Version("1.20.5-pre4"), Version("1.20.6") } }, { 33, { Version("24w18a"), Version("24w20a") } }, + { 34, { Version("24w21a"), Version("1.21") } } +}; + +std::pair ResourcePack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) + { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.hpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.hpp new file mode 100644 index 0000000000..4c9c5ccef2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.hpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "Resource.hpp" +#include "minecraft/mod/DataPack.hpp" + +#include +#include +#include +#include + +class Version; + +// Localized descriptions are fully implemented in the DataPack base class. +// ResourcePack inherits m_localized_descriptions, localizedDescription(), +// setLocalizedDescription(), and allLocalizedDescriptions() from DataPack. + +class ResourcePack : public DataPack +{ + Q_OBJECT + public: + ResourcePack(QObject* parent = nullptr) : DataPack(parent) + {} + ResourcePack(QFileInfo file_info) : DataPack(file_info) + {} + + /** Gets, respectively, the lower and upper versions supported by the set pack format. */ + std::pair compatibleVersions() const override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.cpp new file mode 100644 index 0000000000..a394740515 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ResourcePackFolderModel.hpp" +#include +#include + +#include +#include + +#include "Version.h" + +#include "minecraft/mod/tasks/LocalDataPackParseTask.hpp" + +ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); + m_column_names_translated = QStringList( + { tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, + SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true }; +} + +QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + if (column == PackFormatColumn) + { + auto& resource = at(row); + auto pack_format = resource.packFormat(); + if (pack_format == 0) + return tr("Unrecognized"); + + auto version_bounds = resource.compatibleVersions(); + if (version_bounds.first.toString().isEmpty()) + return QString::number(pack_format); + + return QString("%1 (%2 - %3)") + .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + break; + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::ToolTipRole: + if (column == PackFormatColumn) + { + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case PackFormatColumn: + case DateColumn: + case ImageColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); + default: return {}; + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: return tr("Is the resource pack enabled?"); + case NameColumn: return tr("The name of the resource pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: return tr("The date and time this resource pack was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the resource pack."); + case SizeColumn: return tr("The size of the resource pack."); + default: return {}; + } + case Qt::SizeHintRole: + if (section == ImageColumn) + { + return QSize(64, 0); + } + return {}; + default: return {}; + } +} + +int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Task* ResourcePackFolderModel::createParseTask(Resource& resource) +{ + return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast(&resource)); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.hpp new file mode 100644 index 0000000000..fafd7796c8 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.hpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "ResourceFolderModel.hpp" + +#include "ResourcePack.hpp" + +class ResourcePackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + PackFormatColumn, + DateColumn, + ProviderColumn, + SizeColumn, + NUM_COLUMNS + }; + + explicit ResourcePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + QString id() const override + { + return "resourcepacks"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override + { + return new ResourcePack(file); + } + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(ResourcePack) +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.cpp b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.cpp new file mode 100644 index 0000000000..7814175be6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ +#include "ShaderPack.hpp" + +void ShaderPack::setPackFormat(ShaderPackFormat new_format) +{ + QMutexLocker locker(&m_data_lock); + + m_pack_format = new_format; +} + +bool ShaderPack::valid() const +{ + return m_pack_format != ShaderPackFormat::INVALID; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.hpp b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.hpp new file mode 100644 index 0000000000..71bce711a4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.hpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +/* Info: + * Currently For Optifine / Iris shader packs, + * could be expanded to support others should they exist? + * + * This class and enum are mostly here as placeholders for validating + * that a shaderpack exists and is in the right format, + * namely that they contain a folder named 'shaders'. + * + * In the technical sense it would be possible to parse files like `shaders/shaders.properties` + * to get information like the available profiles but this is not all that useful without more knowledge of the + * shader mod used to be able to change settings. + */ + +#include + +enum class ShaderPackFormat +{ + VALID, + INVALID +}; + +class ShaderPack : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + ShaderPackFormat packFormat() const + { + return m_pack_format; + } + + ShaderPack(QObject* parent = nullptr) : Resource(parent) + {} + ShaderPack(QFileInfo file_info) : Resource(file_info) + {} + + /** Thread-safe. */ + void setPackFormat(ShaderPackFormat new_format); + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ShaderPackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ShaderPackFolderModel.hpp new file mode 100644 index 0000000000..b7fff575a3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ShaderPackFolderModel.hpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "ResourceFolderModel.hpp" +#include "minecraft/mod/ShaderPack.hpp" +#include "minecraft/mod/tasks/LocalShaderPackParseTask.hpp" + +class ShaderPackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + + public: + explicit ShaderPackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr) + : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) + {} + + virtual QString id() const override + { + return "shaderpacks"; + } + + [[nodiscard]] Resource* createResource(const QFileInfo& info) override + { + return new ShaderPack(info); + } + + [[nodiscard]] Task* createParseTask(Resource& resource) override + { + return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); + } + + RESOURCE_HELPERS(ShaderPack); +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePack.cpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.cpp new file mode 100644 index 0000000000..13f644a5d1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "TexturePack.hpp" + +#include +#include +#include "MTPixmapCache.h" + +#include "minecraft/mod/tasks/LocalTexturePackParseTask.hpp" + +void TexturePack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +void TexturePack::setImage(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage( + new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_pack_image_cache_key.key = PixmapCache::insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; +} + +QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) + { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode, Qt::SmoothTransformation); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) + { + return {}; + } + else + { + qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + + // Imaged got evicted from the cache. Re-process it and retry. + TexturePackUtils::processPackPNG(*this); + return image(size); +} + +bool TexturePack::valid() const +{ + return m_description != nullptr; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePack.hpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.hpp new file mode 100644 index 0000000000..2ac75f6ab3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.hpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +#include +#include +#include +#include + +class Version; + +class TexturePack : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + TexturePack(QObject* parent = nullptr) : Resource(parent) + {} + TexturePack(QFileInfo file_info) : Resource(file_info) + {} + + /** Gets the description of the texture pack. */ + QString description() const + { + return m_description; + } + + /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + + /** Thread-safe. */ + void setDescription(QString new_description); + + /** Thread-safe. */ + void setImage(QImage new_image) const; + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + /** The texture pack's description, as defined in the pack.txt file. + */ + QString m_description; + + /** The texture pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct + { + QPixmapCache::Key key; + bool was_ever_used = false; + } mutable m_pack_image_cache_key; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.cpp new file mode 100644 index 0000000000..3f6a7c44e2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ +#include "TexturePackFolderModel.hpp" + +#include "minecraft/mod/tasks/LocalTexturePackParseTask.hpp" +#include "minecraft/mod/tasks/ResourceFolderLoadTask.hpp" + +TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); + m_column_names_translated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, + SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true }; +} + +Task* TexturePackFolderModel::createParseTask(Resource& resource) +{ + return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast(resource)); +} + +QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case DateColumn: + case ImageColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); + default: return {}; + } + case Qt::ToolTipRole: + { + switch (section) + { + case ActiveColumn: return tr("Is the texture pack enabled?"); + case NameColumn: return tr("The name of the texture pack."); + case DateColumn: return tr("The date and time this texture pack was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the texture pack."); + case SizeColumn: return tr("The size of the texture pack."); + default: return {}; + } + } + default: break; + } + + return {}; +} + +int TexturePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.hpp new file mode 100644 index 0000000000..43d2c2d039 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.hpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "ResourceFolderModel.hpp" + +#include "TexturePack.hpp" + +class TexturePackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + DateColumn, + ProviderColumn, + SizeColumn, + NUM_COLUMNS + }; + + explicit TexturePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + virtual QString id() const override + { + return "texturepacks"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override + { + return new TexturePack(file); + } + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(TexturePack) +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/WorldSave.cpp b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.cpp new file mode 100644 index 0000000000..0203ac2159 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "WorldSave.hpp" + +#include "minecraft/mod/tasks/LocalWorldSaveParseTask.hpp" + +void WorldSave::setSaveFormat(WorldSaveFormat new_save_format) +{ + QMutexLocker locker(&m_data_lock); + + m_save_format = new_save_format; +} + +void WorldSave::setSaveDirName(QString dir_name) +{ + QMutexLocker locker(&m_data_lock); + + m_save_dir_name = dir_name; +} + +bool WorldSave::valid() const +{ + return m_save_format != WorldSaveFormat::INVALID; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/WorldSave.hpp b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.hpp new file mode 100644 index 0000000000..dff491a710 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.hpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +#include + +class Version; + +enum class WorldSaveFormat +{ + SINGLE, + MULTI, + INVALID +}; + +class WorldSave : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + WorldSave(QObject* parent = nullptr) : Resource(parent) + {} + WorldSave(QFileInfo file_info) : Resource(file_info) + {} + + /** Gets the format of the save. */ + WorldSaveFormat saveFormat() const + { + return m_save_format; + } + /** Gets the name of the save dir (first found in multi mode). */ + QString saveDirName() const + { + return m_save_dir_name; + } + + /** Thread-safe. */ + void setSaveFormat(WorldSaveFormat new_save_format); + /** Thread-safe. */ + void setSaveDirName(QString dir_name); + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + /** The format in which the save file is in. + * Since saves can be distributed in various slightly different ways, this allows us to treat them separately. + */ + WorldSaveFormat m_save_format = WorldSaveFormat::INVALID; + + QString m_save_dir_name; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp new file mode 100644 index 0000000000..77c60e1a5f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "GetModDependenciesTask.hpp" + +#include +#include +#include +#include "Json.h" +#include "QObjectPtr.h" +#include "minecraft/PackProfile.h" +#include "minecraft/mod/MetadataHandler.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "tasks/SequentialTask.h" +#include "ui/pages/modplatform/ModModel.h" + +static Version mcVersion(BaseInstance* inst) +{ + return static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); +} + +static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) +{ + return static_cast(inst)->getPackProfile()->getSupportedModLoaders().value(); +} + +static bool checkDependencies(std::shared_ptr sel, + Version mcVersion, + ModPlatform::ModLoaderTypes loaders) +{ + return (sel->pack->versions.isEmpty() || sel->version.mcVersion.contains(mcVersion.toString())) + && (!loaders || !sel->version.loaders || sel->version.loaders & loaders); +} + +GetModDependenciesTask::GetModDependenciesTask(BaseInstance* instance, + ModFolderModel* folder, + QList> selected) + : SequentialTask(tr("Get dependencies")), + m_selected(selected), + m_version(mcVersion(instance)), + m_loaderType(mcLoaders(instance)) +{ + for (auto mod : folder->allMods()) + { + m_mods_file_names << mod->fileinfo().fileName(); + if (auto meta = mod->metadata(); meta) + m_mods.append(meta); + } + prepare(); +} + +void GetModDependenciesTask::prepare() +{ + for (auto sel : m_selected) + { + if (checkDependencies(sel, m_version, m_loaderType)) + for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) + { + addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); + } + } +} + +ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName) +{ + if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) + { + auto overide = ModPlatform::getOverrideDeps(); + auto over = + std::find_if(overide.cbegin(), + overide.cend(), + [dep, providerName, isQuilt](const auto& o) + { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); }); + if (over != overide.cend()) + { + return { isQuilt ? over->quilt : over->fabric, dep.type }; + } + } + return dep; +} + +QList GetModDependenciesTask::getDependenciesForVersion( + const ModPlatform::IndexedVersion& version, + const ModPlatform::ResourceProvider providerName) +{ + QList c_dependencies; + for (auto ver_dep : version.dependencies) + { + if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) + continue; + ver_dep = getOverride(ver_dep, providerName); + auto isOnlyVersion = + providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); + if (auto dep = + std::find_if(c_dependencies.begin(), + c_dependencies.end(), + [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) + { return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId; }); + dep != c_dependencies.end()) + continue; // check the current dependency list + + if (auto dep = std::find_if(m_selected.begin(), + m_selected.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) + { + return i->pack->provider == providerName + && (isOnlyVersion ? i->version.version == ver_dep.version + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_selected.end()) + continue; // check the selected versions + + if (auto dep = std::find_if(m_mods.begin(), + m_mods.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) + { + return i->provider == providerName + && (isOnlyVersion ? i->file_id == ver_dep.version + : i->project_id == ver_dep.addonId); + }); + dep != m_mods.end()) + continue; // check the existing mods + + if (auto dep = std::find_if(m_pack_dependencies.begin(), + m_pack_dependencies.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) + { + return i->pack->provider == providerName + && (isOnlyVersion ? i->version.version == ver_dep.addonId + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_pack_dependencies.end()) // check loaded dependencies + continue; + + c_dependencies.append(ver_dep); + } + return c_dependencies; +} + +Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) +{ + auto provider = pDep->pack->provider; + auto responseInfo = std::make_shared(); + auto info = getAPI(provider)->getProject(pDep->pack->addonId.toString(), responseInfo); + connect(info.get(), + &NetJob::succeeded, + [this, responseInfo, provider, pDep] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + removePack(pDep->pack->addonId); + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *responseInfo; + return; + } + try + { + auto obj = provider == ModPlatform::ResourceProvider::FLAME + ? Json::requireObject(Json::requireObject(doc), "data") + : Json::requireObject(doc); + + getAPI(provider)->loadIndexedPack(*pDep->pack, obj); + } + catch (const JSONValidationError& e) + { + removePack(pDep->pack->addonId); + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + }); + return info; +} + +Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName, + int level) +{ + auto pDep = std::make_shared(); + pDep->dependency = dep; + pDep->pack = std::make_shared(); + pDep->pack->addonId = dep.addonId; + pDep->pack->provider = providerName; + + m_pack_dependencies.append(pDep); + + auto provider = providerName; + + auto tasks = makeShared( + QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); + + if (!dep.addonId.toString().isEmpty()) + { + tasks->addTask(getProjectInfoTask(pDep)); + } + + ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; + ResourceAPI::Callback callbacks; + callbacks.on_fail = [](QString reason, int) + { qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); }; + callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) + { + pDep->version = pack; + if (!pDep->version.addonId.isValid()) + { + if (m_loaderType & ModPlatform::Quilt) + { // falback for quilt + auto overide = ModPlatform::getOverrideDeps(); + auto over = + std::find_if(overide.cbegin(), + overide.cend(), + [dep, provider](auto o) { return o.provider == provider && dep.addonId == o.quilt; }); + if (over != overide.cend()) + { + removePack(dep.addonId); + addTask(prepareDependencyTask({ over->fabric, dep.type }, provider, level)); + return; + } + } + removePack(dep.addonId); + return; + } + pDep->version.is_currently_selected = true; + pDep->pack->versions = { pDep->version }; + pDep->pack->versionsLoaded = true; + + if (level == 0) + { + removePack(dep.addonId); + qWarning() << "Dependency cycle exceeded"; + return; + } + if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) + { + pDep->pack->addonId = pDep->version.addonId; + auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider); + if (dep_.addonId != pDep->version.addonId) + { + removePack(pDep->version.addonId); + addTask(prepareDependencyTask(dep_, provider, level)); + } + else + { + addTask(getProjectInfoTask(pDep)); + } + } + if (isLocalyInstalled(pDep)) + { + removePack(pDep->version.addonId); + return; + } + for (auto dep_ : getDependenciesForVersion(pDep->version, provider)) + { + addTask(prepareDependencyTask(dep_, provider, level - 1)); + } + }; + + auto version = getAPI(provider)->getDependencyVersion(std::move(args), std::move(callbacks)); + tasks->addTask(version); + return tasks; +} + +void GetModDependenciesTask::removePack(const QVariant& addonId) +{ + auto pred = [addonId](const std::shared_ptr& v) { return v->pack->addonId == addonId; }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + m_pack_dependencies.removeIf(pred); +#else + for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();) + if (pred(*it)) + it = m_pack_dependencies.erase(it); + else + ++it; +#endif +} + +auto GetModDependenciesTask::getExtraInfo() -> QHash +{ + QHash rby; + auto fullList = m_selected + m_pack_dependencies; + for (auto& mod : fullList) + { + auto addonId = mod->pack->addonId; + auto provider = mod->pack->provider; + auto version = mod->version.fileId; + auto req = QStringList(); + for (auto& smod : fullList) + { + if (provider != smod->pack->provider) + continue; + auto deps = smod->version.dependencies; + if (auto dep = std::find_if(deps.begin(), + deps.end(), + [addonId, provider, version](const ModPlatform::Dependency& d) + { + return d.type == ModPlatform::DependencyType::REQUIRED + && (provider == ModPlatform::ResourceProvider::MODRINTH + && d.addonId.toString().isEmpty() + ? version == d.version + : d.addonId == addonId); + }); + dep != deps.end()) + { + req.append(smod->pack->name); + } + } + rby[addonId.toString()] = { maybeInstalled(mod), req }; + } + return rby; +} + +// super lax compare (but not fuzzy) +// convert to lowercase +// convert all speratores to whitespace +// simplify sequence of internal whitespace to a single space +// efectivly compare two strings ignoring all separators and case +auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) +{ + // allowed character seperators + QList allowedSeperators = { '-', '+', '.', '_' }; + if (excludeDigits) + allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }); + + // copy in lowercase + auto fsName = fsfilename.toLower(); + auto metaName = metadataFilename.toLower(); + + // replace all potential allowed seperatores with whitespace + for (auto sep : allowedSeperators) + { + fsName = fsName.replace(sep, ' '); + metaName = metaName.replace(sep, ' '); + } + + // remove extraneous whitespace + fsName = fsName.simplified(); + metaName = metaName.simplified(); + + return fsName.compare(metaName) == 0; +}; + +bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr pDep) +{ + return pDep->version.fileName.isEmpty() || + + std::find_if( + m_selected.begin(), + m_selected.end(), + [pDep](std::shared_ptr i) + { return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName); }) + != m_selected.end() + || // check the selected versions + + std::find_if(m_mods_file_names.begin(), + m_mods_file_names.end(), + [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) + != m_mods_file_names.end() + || // check the existing mods + + std::find_if(m_pack_dependencies.begin(), + m_pack_dependencies.end(), + [pDep](std::shared_ptr i) + { + return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() + && laxCompare(pDep->version.fileName, i->version.fileName); + }) + != m_pack_dependencies.end(); // check loaded dependencies +} + +bool GetModDependenciesTask::maybeInstalled(std::shared_ptr pDep) +{ + return std::find_if(m_mods_file_names.begin(), + m_mods_file_names.end(), + [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true); }) + != m_mods_file_names.end(); // check the existing mods +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.hpp new file mode 100644 index 0000000000..39fdb95c55 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.hpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "minecraft/mod/MetadataHandler.hpp" +#include "minecraft/mod/ModFolderModel.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "tasks/SequentialTask.h" +#include "tasks/Task.h" +#include "ui/pages/modplatform/ModModel.h" + +class GetModDependenciesTask : public SequentialTask +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + struct PackDependency + { + ModPlatform::Dependency dependency; + ModPlatform::IndexedPack::Ptr pack; + ModPlatform::IndexedVersion version; + PackDependency() = default; + PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v) + { + pack = p; + version = v; + } + }; + + struct PackDependencyExtraInfo + { + bool maybe_installed; + QStringList required_by; + }; + + explicit GetModDependenciesTask(BaseInstance* instance, + ModFolderModel* folder, + QList> selected); + + auto getDependecies() const -> QList> + { + return m_pack_dependencies; + } + QHash getExtraInfo(); + + private: + inline ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) + { + if (provider == ModPlatform::ResourceProvider::FLAME) + return &m_flameAPI; + else + return &m_modrinthAPI; + } + + protected slots: + Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); + QList getDependenciesForVersion(const ModPlatform::IndexedVersion&, + ModPlatform::ResourceProvider providerName); + void prepare(); + Task::Ptr getProjectInfoTask(std::shared_ptr pDep); + ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName); + void removePack(const QVariant& addonId); + + bool isLocalyInstalled(std::shared_ptr pDep); + bool maybeInstalled(std::shared_ptr pDep); + + private: + QList> m_pack_dependencies; + QList> m_mods; + QList> m_selected; + QStringList m_mods_file_names; + + Version m_version; + ModPlatform::ModLoaderTypes m_loaderType; + + ModrinthAPI m_modrinthAPI; + FlameAPI m_flameAPI; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp new file mode 100644 index 0000000000..f2e45edc93 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "LocalDataPackParseTask.hpp" + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/mod/ResourcePack.hpp" + +#include +#include +#include + +#include + +namespace DataPackUtils +{ + + bool process(DataPack* pack, ProcessingLevel level) + { + switch (pack->type()) + { + case ResourceType::FOLDER: return DataPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return DataPackUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for data pack parse task!"; return false; + } + } + + bool processFolder(DataPack* pack, ProcessingLevel level) + { + Q_ASSERT(pack->type() == ResourceType::FOLDER); + + auto mcmeta_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + + QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta")); + if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) + { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return mcmeta_invalid(); // can't open mcmeta file + + auto data = mcmeta_file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + mcmeta_file.close(); + if (!mcmeta_result) + { + return mcmeta_invalid(); // mcmeta invalid + } + } + else + { + return mcmeta_invalid(); // mcmeta file isn't a valid file + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + return true; // only need basic info already checked + } + auto png_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) + { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + + return true; // all tests passed + } + + bool processZIP(DataPack* pack, ProcessingLevel level) + { + Q_ASSERT(pack->type() == ResourceType::ZIPFILE); + + QuaZip zip(pack->fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + auto mcmeta_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + + if (zip.setCurrentFile("pack.mcmeta")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return mcmeta_invalid(); + } + + auto data = file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + file.close(); + if (!mcmeta_result) + { + return mcmeta_invalid(); // mcmeta invalid + } + } + else + { + return mcmeta_invalid(); // could not set pack.mcmeta as current file. + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; // only need basic info already checked + } + + auto png_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + zip.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + zip.close(); + + return true; + } + + // https://minecraft.wiki/w/Data_pack#pack.mcmeta + // https://minecraft.wiki/w/Raw_JSON_text_format + // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + bool processMCMeta(DataPack* pack, QByteArray&& raw_data) + { + try + { + auto json_doc = QJsonDocument::fromJson(raw_data); + auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); + + pack->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); + pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); + } + catch (Json::JsonException& e) + { + qWarning() << "JsonException: " << e.what() << e.cause(); + return false; + } + return true; + } + + QString buildStyle(const QJsonObject& obj) + { + QStringList styles; + if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) + { + styles << QString("color: %1;").arg(color); + } + if (obj.contains("bold")) + { + QString weight = "normal"; + if (Json::ensureBoolean(obj, "bold", false)) + { + weight = "bold"; + } + styles << QString("font-weight: %1;").arg(weight); + } + if (obj.contains("italic")) + { + QString style = "normal"; + if (Json::ensureBoolean(obj, "italic", false)) + { + style = "italic"; + } + styles << QString("font-style: %1;").arg(style); + } + + return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); + } + + QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) + { + QString result; + for (auto current : value) + result += processComponent(current, strikethrough, underline); + return result; + } + + QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) + { + underline = Json::ensureBoolean(obj, "underlined", underline); + strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); + + QString result = Json::ensureString(obj, "text"); + if (underline) + { + result = QString("%1").arg(result); + } + if (strikethrough) + { + result = QString("%1").arg(result); + } + // the extra needs to be a array + result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); + if (auto style = buildStyle(obj); !style.isEmpty()) + { + result = QString("%2").arg(style, result); + } + if (obj.contains("clickEvent")) + { + auto click_event = Json::ensureObject(obj, "clickEvent"); + auto action = Json::ensureString(click_event, "action"); + auto value = Json::ensureString(click_event, "value"); + if (action == "open_url" && !value.isEmpty()) + { + result = QString("%2").arg(value, result); + } + } + return result; + } + + QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) + { + if (value.isString()) + { + return value.toString(); + } + if (value.isBool()) + { + return value.toBool() ? "true" : "false"; + } + if (value.isDouble()) + { + return QString::number(value.toDouble()); + } + if (value.isArray()) + { + return processComponent(value.toArray(), strikethrough, underline); + } + if (value.isObject()) + { + return processComponent(value.toObject(), strikethrough, underline); + } + qWarning() << "Invalid component type!"; + return {}; + } + + bool processPackPNG(const DataPack* pack, QByteArray&& raw_data) + { + auto img = QImage::fromData(raw_data); + if (!img.isNull()) + { + pack->setImage(img); + } + else + { + qWarning() << "Failed to parse pack.png."; + return false; + } + return true; + } + + bool processPackPNG(const DataPack* pack) + { + auto png_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack->type()) + { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) + { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return false; + } + case ResourceType::ZIPFILE: + { + QuaZip zip(pack->fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // could not set pack.mcmeta as current file. + } + return false; + } + default: qWarning() << "Invalid type for data pack parse task!"; return false; + } + } + + bool validate(QFileInfo file) + { + DataPack dp{ file }; + return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); + } + + bool validateResourcePack(QFileInfo file) + { + ResourcePack rp{ file }; + return DataPackUtils::process(&rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); + } + +} // namespace DataPackUtils + +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) +{} + +void LocalDataPackParseTask::executeTask() +{ + if (!DataPackUtils::process(m_data_pack)) + { + emitFailed("process failed"); + return; + } + + emitSucceeded(); +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.hpp new file mode 100644 index 0000000000..aca6d76595 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.hpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +#include "minecraft/mod/DataPack.hpp" + +#include "tasks/Task.h" + +namespace DataPackUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processMCMeta(DataPack* pack, QByteArray&& raw_data); + + QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); + + bool processPackPNG(const DataPack* pack, QByteArray&& raw_data); + + /// processes ONLY the pack.png (rest of the pack may be invalid) + bool processPackPNG(const DataPack* pack); + + /** Checks whether a file is valid as a data pack or not. */ + bool validate(QFileInfo file); + + /** Checks whether a file is valid as a resource pack or not. */ + bool validateResourcePack(QFileInfo file); + +} // namespace DataPackUtils + +class LocalDataPackParseTask : public Task +{ + Q_OBJECT + public: + LocalDataPackParseTask(int token, DataPack* dp); + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + DataPack* m_data_pack; +}; \ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.cpp new file mode 100644 index 0000000000..2d74df5565 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -0,0 +1,942 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "LocalModParseTask.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/mod/ModDetails.hpp" +#include "settings/INIFile.h" + +static const QRegularExpression s_newlineRegex("\r\n|\n|\r"); + +namespace ModUtils +{ + + // NEW format + // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/c8d8f1929aff9979e322af79a59ce81f3e02db6a + + // OLD format: + // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc + ModDetails ReadMCModInfo(QByteArray contents) + { + auto getInfoFromArray = [](QJsonArray arr) -> ModDetails + { + if (!arr.at(0).isObject()) + { + return {}; + } + ModDetails details; + auto firstObj = arr.at(0).toObject(); + details.mod_id = firstObj.value("modid").toString(); + auto name = firstObj.value("name").toString(); + // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name + if (name != "Example Mod") + { + details.name = name; + } + details.version = firstObj.value("version").toString(); + auto homeurl = firstObj.value("url").toString().trimmed(); + if (!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details.homeurl = homeurl; + details.description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) + { + // NOTE: 'authors' is used as a fallback or alternative key for 'authorList' in some metadata formats. + authors = firstObj.value("authors").toArray(); + } + + if (firstObj.contains("logoFile")) + { + details.icon_file = firstObj.value("logoFile").toString(); + } + + for (auto author : authors) + { + details.authors.append(author.toString()); + } + return details; + }; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + return getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if (val.isUndefined()) + { + val = jsonDoc.object().value("modListVersion"); + } + + int version = Json::ensureInteger(val, -1); + + // Some mods set the number with "", so it's a String instead + if (version < 0) + version = Json::ensureString(val, "").toInt(); + + if (version != 2) + { + qWarning() << QString( + R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)") + .arg(version); + qWarning() << "The contents of 'mcmod.info' are as follows:"; + qWarning() << contents; + } + + auto arrVal = jsonDoc.object().value("modlist"); + if (arrVal.isUndefined()) + { + arrVal = jsonDoc.object().value("modList"); + } + if (arrVal.isArray()) + { + return getInfoFromArray(arrVal.toArray()); + } + } + return {}; + } + + // https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md + ModDetails ReadMCModTOML(QByteArray contents) + { + ModDetails details; + + toml::table tomlData; +#if TOML_EXCEPTIONS + try + { + tomlData = toml::parse(contents.toStdString()); + } + catch ([[maybe_unused]] const toml::parse_error& err) + { + return {}; + } +#else + toml::parse_result result = toml::parse(contents.toStdString()); + if (!result) + { + return {}; + } + tomlData = result.table(); +#endif + + // array defined by [[mods]] + auto tomlModsArr = tomlData["mods"].as_array(); + if (!tomlModsArr) + { + qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; + return {}; + } + + // we only really care about the first element, since multiple mods in one file is not supported by us at the + // moment + auto tomlModsTable0 = tomlModsArr->get(0); + if (!tomlModsTable0) + { + qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; + return {}; + } + auto modsTable = tomlModsTable0->as_table(); + if (!modsTable) + { + qWarning() << "Corrupted mods.toml? [[mods]] was not a table!"; + return {}; + } + + // mandatory properties - always in [[mods]] + if (auto modIdDatum = (*modsTable)["modId"].as_string()) + { + details.mod_id = QString::fromStdString(modIdDatum->get()); + } + if (auto versionDatum = (*modsTable)["version"].as_string()) + { + details.version = QString::fromStdString(versionDatum->get()); + } + if (auto displayNameDatum = (*modsTable)["displayName"].as_string()) + { + details.name = QString::fromStdString(displayNameDatum->get()); + } + if (auto descriptionDatum = (*modsTable)["description"].as_string()) + { + details.description = QString::fromStdString(descriptionDatum->get()); + } + + // optional properties - can be in the root table or [[mods]] + QString authors = ""; + if (auto authorsDatum = tomlData["authors"].as_string()) + { + authors = QString::fromStdString(authorsDatum->get()); + } + else if (auto authorsDatumMods = (*modsTable)["authors"].as_string()) + { + authors = QString::fromStdString(authorsDatumMods->get()); + } + if (!authors.isEmpty()) + { + details.authors.append(authors); + } + + QString homeurl = ""; + if (auto homeurlDatum = tomlData["displayURL"].as_string()) + { + homeurl = QString::fromStdString(homeurlDatum->get()); + } + else if (auto homeurlDatumMods = (*modsTable)["displayURL"].as_string()) + { + homeurl = QString::fromStdString(homeurlDatumMods->get()); + } + // fix up url. + if (!homeurl.isEmpty() && !homeurl.startsWith("http://") && !homeurl.startsWith("https://") + && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + details.homeurl = homeurl; + + QString issueTrackerURL = ""; + if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) + { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); + } + else if (auto issueTrackerURLDatumMods = (*modsTable)["issueTrackerURL"].as_string()) + { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatumMods->get()); + } + details.issue_tracker = issueTrackerURL; + + QString license = ""; + if (auto licenseDatum = tomlData["license"].as_string()) + { + license = QString::fromStdString(licenseDatum->get()); + } + else if (auto licenseDatumMods = (*modsTable)["license"].as_string()) + { + license = QString::fromStdString(licenseDatumMods->get()); + } + if (!license.isEmpty()) + details.licenses.append(ModLicense(license)); + + QString logoFile = ""; + if (auto logoFileDatum = tomlData["logoFile"].as_string()) + { + logoFile = QString::fromStdString(logoFileDatum->get()); + } + else if (auto logoFileDatumMods = (*modsTable)["logoFile"].as_string()) + { + logoFile = QString::fromStdString(logoFileDatumMods->get()); + } + details.icon_file = logoFile; + + return details; + } + + // https://fabricmc.net/wiki/documentation:fabric_mod_json + ModDetails ReadFabricModInfo(QByteArray contents) + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; + + ModDetails details; + + details.mod_id = object.value("id").toString(); + details.version = object.value("version").toString(); + + details.name = object.contains("name") ? object.value("name").toString() : details.mod_id; + details.description = object.value("description").toString(); + + if (schemaVersion >= 1) + { + QJsonArray authors = object.value("authors").toArray(); + for (auto author : authors) + { + if (author.isObject()) + { + details.authors.append(author.toObject().value("name").toString()); + } + else + { + details.authors.append(author.toString()); + } + } + + if (object.contains("contact")) + { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) + { + details.homeurl = contact.value("homepage").toString(); + } + if (contact.contains("issues")) + { + details.issue_tracker = contact.value("issues").toString(); + } + } + + if (object.contains("license")) + { + auto license = object.value("license"); + if (license.isArray()) + { + for (auto l : license.toArray()) + { + if (l.isString()) + { + details.licenses.append(ModLicense(l.toString())); + } + else if (l.isObject()) + { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + } + else if (license.isString()) + { + details.licenses.append(ModLicense(license.toString())); + } + else if (license.isObject()) + { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (object.contains("icon")) + { + auto icon = object.value("icon"); + if (icon.isObject()) + { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) + { + auto size = key.split('x').first().toInt(); + if (size > largest) + { + largest = size; + } + } + if (largest > 0) + { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } + else + { // parsing the sizes failed + // take the first + if (auto it = obj.begin(); it != obj.end()) + { + details.icon_file = it->toString(); + } + } + } + else if (icon.isString()) + { + details.icon_file = icon.toString(); + } + } + } + return details; + } + + // https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md + ModDetails ReadQuiltModInfo(QByteArray contents) + { + ModDetails details; + try + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); + auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); + + // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md + if (schemaVersion == 1) + { + auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); + + details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); + details.version = Json::requireString(modInfo.value("version"), "Mod version"); + + auto modMetadata = Json::ensureObject(modInfo.value("metadata")); + + details.name = Json::ensureString(modMetadata.value("name"), details.mod_id); + details.description = Json::ensureString(modMetadata.value("description")); + + auto modContributors = Json::ensureObject(modMetadata.value("contributors")); + + // We don't really care about the role of a contributor here + details.authors += modContributors.keys(); + + auto modContact = Json::ensureObject(modMetadata.value("contact")); + + if (modContact.contains("homepage")) + { + details.homeurl = Json::requireString(modContact.value("homepage")); + } + if (modContact.contains("issues")) + { + details.issue_tracker = Json::requireString(modContact.value("issues")); + } + + if (modMetadata.contains("license")) + { + auto license = modMetadata.value("license"); + if (license.isArray()) + { + for (auto l : license.toArray()) + { + if (l.isString()) + { + details.licenses.append(ModLicense(l.toString())); + } + else if (l.isObject()) + { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + } + else if (license.isString()) + { + details.licenses.append(ModLicense(license.toString())); + } + else if (license.isObject()) + { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (modMetadata.contains("icon")) + { + auto icon = modMetadata.value("icon"); + if (icon.isObject()) + { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) + { + auto size = key.split('x').first().toInt(); + if (size > largest) + { + largest = size; + } + } + if (largest > 0) + { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } + else + { // parsing the sizes failed + // take the first + if (auto it = obj.begin(); it != obj.end()) + { + details.icon_file = it->toString(); + } + } + } + else if (icon.isString()) + { + details.icon_file = icon.toString(); + } + } + } + } + catch (const Exception& e) + { + qWarning() << "Unable to parse mod info:" << e.cause(); + } + return details; + } + + ModDetails ReadForgeInfo(QByteArray contents) + { + ModDetails details; + // Read the data + details.name = "Minecraft Forge"; + details.mod_id = "Forge"; + details.homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return details; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + details.version = major + "." + minor + "." + revision + "." + build; + return details; + } + + ModDetails ReadLiteModInfo(QByteArray contents) + { + ModDetails details; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + details.mod_id = details.name = object.value("name").toString(); + } + if (object.contains("version")) + { + details.version = object.value("version").toString(""); + } + else + { + details.version = object.value("revision").toString(""); + } + details.mcversion = object.value("mcversion").toString(); + auto author = object.value("author").toString(); + if (!author.isEmpty()) + { + details.authors.append(author); + } + details.description = object.value("description").toString(); + details.homeurl = object.value("url").toString(); + return details; + } + + // https://git.sleeping.town/unascribed/NilLoader/src/commit/d7fc87b255fc31019ff90f80d45894927fac6efc/src/main/java/nilloader/api/NilMetadata.java#L64 + ModDetails ReadNilModInfo(QByteArray contents, QString fname) + { + ModDetails details; + + QDCSS cssData = QDCSS(contents); + auto name = cssData.get("@nilmod.name"); + auto desc = cssData.get("@nilmod.description"); + auto authors = cssData.get("@nilmod.authors"); + + if (name->has_value()) + { + details.name = name->value(); + } + if (desc->has_value()) + { + details.description = desc->value(); + } + if (authors->has_value()) + { + details.authors.append(authors->value()); + } + details.version = cssData.get("@nilmod.version")->value_or("?"); + + details.mod_id = fname.remove(".nilmod.css"); + + return details; + } + + bool process(Mod& mod, ProcessingLevel level) + { + switch (mod.type()) + { + case ResourceType::FOLDER: return processFolder(mod, level); + case ResourceType::ZIPFILE: return processZIP(mod, level); + case ResourceType::LITEMOD: return processLitemod(mod); + default: qWarning() << "Invalid type for mod parse task!"; return false; + } + } + + bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) + { + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadMCModTOML(file.readAll()); + file.close(); + + // to replace ${file.jarVersion} with the actual version, as needed + if (details.version == "${file.jarVersion}") + { + if (zip.setCurrentFile("META-INF/MANIFEST.MF")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + // quick and dirty line-by-line parser + auto manifestLines = QString(file.readAll()).split(s_newlineRegex); + QString manifestVersion = ""; + for (auto& line : manifestLines) + { + if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) + { + manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); + break; + } + } + + // some mods use ${projectversion} in their build.gradle, causing this mess to show up in + // MANIFEST.MF also keep with forge's behavior of setting the version to "NONE" if none is found + if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") + { + manifestVersion = "NONE"; + } + + details.version = manifestVersion; + + file.close(); + } + } + + zip.close(); + mod.setDetails(details); + + return true; + } + else if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("quilt.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadQuiltModInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("fabric.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadFabricModInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("META-INF/nil/mappings.json")) + { + // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename + // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time + + QString foundNilMeta; + for (auto& fname : zip.getFileNameList()) + { + // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own + // meta file + if (fname.endsWith(".nilmod.css") && fname != "nilloader.nilmod.css") + { + foundNilMeta = fname; + break; + } + } + + if (zip.setCurrentFile(foundNilMeta)) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadNilModInfo(file.readAll(), foundNilMeta); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + } + + zip.close(); + return false; // no valid mod found in archive + } + + bool processFolder(Mod& mod, [[maybe_unused]] ProcessingLevel level) + { + ModDetails details; + + QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info")); + if (mcmod_info.exists() && mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return false; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return false; + details = ReadMCModInfo(data); + + mod.setDetails(details); + return true; + } + + return false; // no valid mcmod.info file found + } + + bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level) + { + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadLiteModInfo(file.readAll()); + file.close(); + + mod.setDetails(details); + return true; + } + zip.close(); + + return false; // no valid litemod.json found in archive + } + + /** Checks whether a file is valid as a mod or not. */ + bool validate(QFileInfo file) + { + Mod mod{ file }; + return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); + } + + bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap) + { + auto img = QImage::fromData(raw_data); + if (!img.isNull()) + { + *pixmap = mod.setIcon(img); + } + else + { + qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); + return false; + } + return true; + } + + bool loadIconFile(const Mod& mod, QPixmap* pixmap) + { + if (mod.iconPath().isEmpty()) + { + qWarning() << "No Iconfile set, be sure to parse the mod first"; + return false; + } + + auto png_invalid = [&mod](const QString& reason) + { + qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon:" << reason; + return false; + }; + + switch (mod.type()) + { + case ResourceType::FOLDER: + { + QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); + if (icon_info.exists() && icon_info.isFile()) + { + QFile icon(icon_info.filePath()); + if (!icon.open(QIODevice::ReadOnly)) + { + return png_invalid("failed to open file " + icon_info.filePath()); + } + auto data = icon.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); + + icon.close(); + + if (!icon_result) + { + return png_invalid("invalid png image"); // icon invalid + } + return true; + } + return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); + } + case ResourceType::ZIPFILE: + { + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive"); + + QuaZipFile file(&zip); + + if (zip.setCurrentFile(mod.iconPath())) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive"); + } + + auto data = file.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); + + file.close(); + if (!icon_result) + { + return png_invalid("invalid png image"); // icon png invalid + } + return true; + } + return png_invalid("Failed to set '" + mod.iconPath() + "' as current file in zip archive"); // could + // not set + // icon as + // current + // file. + } + case ResourceType::LITEMOD: + { + return png_invalid("litemods do not have icons"); // can lightmods even have icons? + } + default: return png_invalid("Invalid type for mod, can not load icon."); + } + } + +} // namespace ModUtils + +LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) + : Task(false), + m_token(token), + m_type(type), + m_modFile(modFile), + m_result(new Result()) +{} + +bool LocalModParseTask::abort() +{ + m_aborted.store(true); + return true; +} + +void LocalModParseTask::executeTask() +{ + Mod mod{ m_modFile }; + ModUtils::process(mod, ModUtils::ProcessingLevel::Full); + + m_result->details = mod.details(); + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.hpp new file mode 100644 index 0000000000..29d87d5746 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include + +#include "minecraft/mod/Mod.hpp" +#include "minecraft/mod/ModDetails.hpp" + +#include "tasks/Task.h" + +namespace ModUtils +{ + + ModDetails ReadFabricModInfo(QByteArray contents); + ModDetails ReadQuiltModInfo(QByteArray contents); + ModDetails ReadForgeInfo(QByteArray contents); + ModDetails ReadLiteModInfo(QByteArray contents); + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + + /** Checks whether a file is valid as a mod or not. */ + bool validate(QFileInfo file); + + bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap); + bool loadIconFile(const Mod& mod, QPixmap* pixmap); +} // namespace ModUtils + +class LocalModParseTask : public Task +{ + Q_OBJECT + public: + struct Result + { + ModDetails details; + }; + using ResultPtr = std::shared_ptr; + ResultPtr result() const + { + return m_result; + } + + bool canAbort() const override + { + return true; + } + bool abort() override; + + LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + ResourceType m_type; + QFileInfo m_modFile; + ResultPtr m_result; + + std::atomic m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.cpp new file mode 100644 index 0000000000..a2de759c31 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include + +#include "LocalResourceParse.hpp" + +#include "LocalDataPackParseTask.hpp" +#include "LocalModParseTask.hpp" +#include "LocalShaderPackParseTask.hpp" +#include "LocalTexturePackParseTask.hpp" +#include "LocalWorldSaveParseTask.hpp" +#include "modplatform/ResourceType.h" + +namespace ResourceUtils +{ + ModPlatform::ResourceType identify(QFileInfo file) + { + if (file.exists() && file.isFile()) + { + if (ModUtils::validate(file)) + { + // mods can contain resource and data packs so they must be tested first + qDebug() << file.fileName() << "is a mod"; + return ModPlatform::ResourceType::Mod; + } + else if (DataPackUtils::validateResourcePack(file)) + { + qDebug() << file.fileName() << "is a resource pack"; + return ModPlatform::ResourceType::ResourcePack; + } + else if (TexturePackUtils::validate(file)) + { + qDebug() << file.fileName() << "is a pre 1.6 texture pack"; + return ModPlatform::ResourceType::TexturePack; + } + else if (DataPackUtils::validate(file)) + { + qDebug() << file.fileName() << "is a data pack"; + return ModPlatform::ResourceType::DataPack; + } + else if (WorldSaveUtils::validate(file)) + { + qDebug() << file.fileName() << "is a world save"; + return ModPlatform::ResourceType::World; + } + else if (ShaderPackUtils::validate(file)) + { + qDebug() << file.fileName() << "is a shader pack"; + return ModPlatform::ResourceType::ShaderPack; + } + else + { + qDebug() << "Can't Identify" << file.fileName(); + } + } + else + { + qDebug() << "Can't find" << file.absolutePath(); + } + return ModPlatform::ResourceType::Unknown; + } + +} // namespace ResourceUtils diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.hpp new file mode 100644 index 0000000000..757e4777f1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.hpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include "modplatform/ResourceType.h" + +namespace ResourceUtils +{ + ModPlatform::ResourceType identify(QFileInfo file); +} // namespace ResourceUtils diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp new file mode 100644 index 0000000000..19e47d334f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "LocalResourceUpdateTask.hpp" + +#include "FileSystem.h" +#include "minecraft/mod/MetadataHandler.hpp" + +#ifdef Q_OS_WIN32 +#include +#endif + +LocalResourceUpdateTask::LocalResourceUpdateTask(QDir index_dir, + ModPlatform::IndexedPack& project, + ModPlatform::IndexedVersion& version) + : m_index_dir(index_dir), + m_project(project), + m_version(version) +{ + // Ensure a '.index' folder exists in the mods folder, and create it if it does not + if (!FS::ensureFolderPathExists(index_dir.path())) + { + emitFailed(QString("Unable to create index directory at %1!").arg(index_dir.absolutePath())); + } + +#ifdef Q_OS_WIN32 + SetFileAttributesW(index_dir.path().toStdWString().c_str(), + FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif +} + +void LocalResourceUpdateTask::executeTask() +{ + setStatus(tr("Updating index for resource:\n%1").arg(m_project.name)); + + auto old_metadata = Metadata::get(m_index_dir, m_project.addonId); + if (old_metadata.isValid()) + { + emit hasOldResource(old_metadata.name, old_metadata.filename); + if (m_project.slug.isEmpty()) + m_project.slug = old_metadata.slug; + } + + auto pw_mod = Metadata::create(m_index_dir, m_project, m_version); + if (pw_mod.isValid()) + { + Metadata::update(m_index_dir, pw_mod); + emitSucceeded(); + } + else + { + qCritical() << "Tried to update an invalid resource!"; + emitFailed(tr("Invalid metadata")); + } +} + +auto LocalResourceUpdateTask::abort() -> bool +{ + emitAborted(); + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.hpp new file mode 100644 index 0000000000..8c4bc81e78 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.hpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include + +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class LocalResourceUpdateTask : public Task +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + explicit LocalResourceUpdateTask(QDir index_dir, + ModPlatform::IndexedPack& project, + ModPlatform::IndexedVersion& version); + + auto canAbort() const -> bool override + { + return true; + } + auto abort() -> bool override; + + protected slots: + //! Entry point for tasks. + void executeTask() override; + + signals: + void hasOldResource(QString name, QString filename); + + private: + QDir m_index_dir; + ModPlatform::IndexedPack m_project; + ModPlatform::IndexedVersion m_version; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp new file mode 100644 index 0000000000..376b99506d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "LocalShaderPackParseTask.hpp" + +#include "FileSystem.h" + +#include +#include +#include + +namespace ShaderPackUtils +{ + + bool process(ShaderPack& pack, ProcessingLevel level) + { + switch (pack.type()) + { + case ResourceType::FOLDER: return ShaderPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return ShaderPackUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for shader pack parse task!"; return false; + } + } + + bool processFolder(ShaderPack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders")); + if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) + { + return false; // assets dir does not exists or isn't valid + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) + { + return true; // only need basic info already checked + } + + return true; // all tests passed + } + + bool processZIP(ShaderPack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/shaders")) + { + return false; // assets dir does not exists at zip root + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; // only need basic info already checked + } + + zip.close(); + + return true; + } + + bool validate(QFileInfo file) + { + ShaderPack sp{ file }; + return ShaderPackUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); + } + +} // namespace ShaderPackUtils + +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) + : Task(false), + m_token(token), + m_shader_pack(sp) +{} + +bool LocalShaderPackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalShaderPackParseTask::executeTask() +{ + if (!ShaderPackUtils::process(m_shader_pack)) + { + emitFailed("this is not a shader pack"); + return; + } + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.hpp new file mode 100644 index 0000000000..2d74ce0116 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +#include "minecraft/mod/ShaderPack.hpp" + +#include "tasks/Task.h" + +namespace ShaderPackUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + + /** Checks whether a file is valid as a shader pack or not. */ + bool validate(QFileInfo file); +} // namespace ShaderPackUtils + +class LocalShaderPackParseTask : public Task +{ + Q_OBJECT + public: + LocalShaderPackParseTask(int token, ShaderPack& sp); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + ShaderPack& m_shader_pack; + + bool m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp new file mode 100644 index 0000000000..fe7f20b730 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "LocalTexturePackParseTask.hpp" + +#include "FileSystem.h" + +#include +#include + +#include + +namespace TexturePackUtils +{ + + bool process(TexturePack& pack, ProcessingLevel level) + { + switch (pack.type()) + { + case ResourceType::FOLDER: return TexturePackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return TexturePackUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for resource pack parse task!"; return false; + } + } + + bool processFolder(TexturePack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.txt")); + if (mcmeta_file_info.isFile()) + { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return false; + + auto data = mcmeta_file.readAll(); + + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); + + mcmeta_file.close(); + if (!packTXT_result) + { + return false; + } + } + else + { + return false; + } + + if (level == ProcessingLevel::BasicInfoOnly) + return true; + + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.isFile()) + { + QFile mcmeta_file(image_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return false; + + auto data = mcmeta_file.readAll(); + + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + mcmeta_file.close(); + if (!packPNG_result) + { + return false; + } + } + else + { + return false; + } + + return true; + } + + bool processZIP(TexturePack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("pack.txt")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return false; + } + + auto data = file.readAll(); + + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); + + file.close(); + if (!packTXT_result) + { + return false; + } + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; + } + + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return false; + } + + auto data = file.readAll(); + + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + zip.close(); + if (!packPNG_result) + { + return false; + } + } + + zip.close(); + + return true; + } + + bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) + { + pack.setDescription(QString(raw_data)); + return true; + } + + bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data) + { + auto img = QImage::fromData(raw_data); + if (!img.isNull()) + { + pack.setImage(img); + } + else + { + qWarning() << "Failed to parse pack.png."; + return false; + } + return true; + } + + bool processPackPNG(const TexturePack& pack) + { + auto png_invalid = [&pack]() + { + qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack.type()) + { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) + { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return false; + } + case ResourceType::ZIPFILE: + { + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) + { + zip.close(); + return png_invalid(); // pack.png invalid + } + } + else + { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + return false; + } + default: qWarning() << "Invalid type for resource pack parse task!"; return false; + } + } + + bool validate(QFileInfo file) + { + TexturePack rp{ file }; + return TexturePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); + } + +} // namespace TexturePackUtils + +LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) + : Task(false), + m_token(token), + m_texture_pack(rp) +{} + +bool LocalTexturePackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalTexturePackParseTask::executeTask() +{ + if (!TexturePackUtils::process(m_texture_pack)) + { + emitFailed("this is not a texture pack"); + return; + } + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.hpp new file mode 100644 index 0000000000..b851928d6b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.hpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +#include "minecraft/mod/TexturePack.hpp" + +#include "tasks/Task.h" + +namespace TexturePackUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); + bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data); + + /// processes ONLY the pack.png (rest of the pack may be invalid) + bool processPackPNG(const TexturePack& pack); + + /** Checks whether a file is valid as a texture pack or not. */ + bool validate(QFileInfo file); +} // namespace TexturePackUtils + +class LocalTexturePackParseTask : public Task +{ + Q_OBJECT + public: + LocalTexturePackParseTask(int token, TexturePack& rp); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + TexturePack& m_texture_pack; + + bool m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp new file mode 100644 index 0000000000..b2cb4d0bc9 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "LocalWorldSaveParseTask.hpp" + +#include "FileSystem.h" + +#include +#include +#include + +#include +#include + +namespace WorldSaveUtils +{ + + bool process(WorldSave& pack, ProcessingLevel level) + { + switch (pack.type()) + { + case ResourceType::FOLDER: return WorldSaveUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return WorldSaveUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for world save parse task!"; return false; + } + } + + /// @brief checks a folder structure to see if it contains a level.dat + /// @param dir the path to check + /// @param saves used in recursive call if a "saves" dir was found + /// @return std::tuple of ( + /// bool , + /// QString , + /// bool + /// ) + static std::tuple contains_level_dat(QDir dir, bool saves = false) + { + for (auto const& entry : dir.entryInfoList()) + { + if (!entry.isDir()) + { + continue; + } + if (!saves && entry.fileName() == "saves") + { + return contains_level_dat(QDir(entry.filePath()), true); + } + QFileInfo level_dat(FS::PathCombine(entry.filePath(), "level.dat")); + if (level_dat.exists() && level_dat.isFile()) + { + return std::make_tuple(true, entry.fileName(), saves); + } + } + return std::make_tuple(false, "", saves); + } + + bool processFolder(WorldSave& save, ProcessingLevel level) + { + Q_ASSERT(save.type() == ResourceType::FOLDER); + + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(QDir(save.fileinfo().filePath())); + + if (!found) + { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) + { + save.setSaveFormat(WorldSaveFormat::MULTI); + } + else + { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + return true; // only need basic info already checked + } + + // reserved for more intensive processing + + return true; // all tests passed + } + + /// @brief checks a folder structure to see if it contains a level.dat + /// @param zip the zip file to check + /// @return std::tuple of ( + /// bool , + /// QString , + /// bool + /// ) + static std::tuple contains_level_dat(QuaZip& zip) + { + bool saves = false; + QuaZipDir zipDir(&zip); + if (zipDir.exists("/saves")) + { + saves = true; + zipDir.cd("/saves"); + } + + for (auto const& entry : zipDir.entryList()) + { + zipDir.cd(entry); + if (zipDir.exists("level.dat")) + { + return std::make_tuple(true, entry, saves); + } + zipDir.cd(".."); + } + return std::make_tuple(false, "", saves); + } + + bool processZIP(WorldSave& save, ProcessingLevel level) + { + Q_ASSERT(save.type() == ResourceType::ZIPFILE); + + QuaZip zip(save.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip); + + if (save_dir_name.endsWith("/")) + { + save_dir_name.chop(1); + } + + if (!found) + { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) + { + save.setSaveFormat(WorldSaveFormat::MULTI); + } + else + { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; // only need basic info already checked + } + + // reserved for more intensive processing + + zip.close(); + + return true; + } + + bool validate(QFileInfo file) + { + WorldSave sp{ file }; + return WorldSaveUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); + } + +} // namespace WorldSaveUtils + +LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(false), m_token(token), m_save(save) +{} + +bool LocalWorldSaveParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalWorldSaveParseTask::executeTask() +{ + if (!WorldSaveUtils::process(m_save)) + { + emitFailed("this is not a world"); + return; + } + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.hpp new file mode 100644 index 0000000000..2d1eb1fac1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +#include "minecraft/mod/WorldSave.hpp" + +#include "tasks/Task.h" + +namespace WorldSaveUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool validate(QFileInfo file); + +} // namespace WorldSaveUtils + +class LocalWorldSaveParseTask : public Task +{ + Q_OBJECT + public: + LocalWorldSaveParseTask(int token, WorldSave& save); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + WorldSave& m_save; + + bool m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp new file mode 100644 index 0000000000..49088d4dcd --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "ResourceFolderLoadTask.hpp" + +#include "Application.h" +#include "FileSystem.h" +#include "minecraft/mod/MetadataHandler.hpp" + +#include + +ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function create_function) + : Task(false), + m_resource_dir(resource_dir), + m_index_dir(index_dir), + m_is_indexed(is_indexed), + m_clean_orphan(clean_orphan), + m_create_func(create_function), + m_result(new Result()), + m_thread_to_spawn_into(thread()) +{} + +void ResourceFolderLoadTask::executeTask() +{ + if (thread() != m_thread_to_spawn_into) + connect(this, &Task::finished, this->thread(), &QThread::quit); + + if (m_is_indexed) + { + // Read metadata first + getFromMetadata(); + } + + // Read JAR files that don't have metadata + m_resource_dir.refresh(); + for (auto entry : m_resource_dir.entryInfoList()) + { + // The `.index` directory is an internal metadata store and should never be treated as a resource. + // On Unix it is usually hidden by name (and therefore excluded by QDir filters), but on Windows + // it may be visible if the hidden attribute wasn't applied yet. + if (entry.fileName() == ".index") + { + continue; + } + + auto filePath = entry.absoluteFilePath(); + if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) + { + continue; + } + auto newFilePath = FS::getUniqueResourceName(filePath); + if (newFilePath != filePath) + { + FS::move(filePath, newFilePath); + entry = QFileInfo(newFilePath); + } + + Resource* resource = m_create_func(entry); + + if (resource->enabled()) + { + if (m_result->resources.contains(resource->internal_id())) + { + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); + // Delete the object we just created, since a valid one is already in the mods list. + delete resource; + } + else + { + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); + } + } + else + { + QString chopped_id = resource->internal_id().chopped(9); + if (m_result->resources.contains(chopped_id)) + { + m_result->resources[resource->internal_id()].reset(resource); + + auto metadata = m_result->resources[chopped_id]->metadata(); + if (metadata) + { + resource->setMetadata(*metadata); + + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); + m_result->resources.remove(chopped_id); + } + } + else + { + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); + } + } + } + + // Remove orphan metadata to prevent issues + // See https://github.com/PolyMC/PolyMC/issues/996 + if (m_clean_orphan) + { + QMutableMapIterator iter(m_result->resources); + while (iter.hasNext()) + { + auto resource = iter.next().value(); + if (resource->status() == ResourceStatus::NOT_INSTALLED) + { + resource->destroy(m_index_dir, false, false); + iter.remove(); + } + } + } + + for (auto mod : m_result->resources) + mod->moveToThread(m_thread_to_spawn_into); + + if (m_aborted) + emit finished(); + else + emitSucceeded(); +} + +void ResourceFolderLoadTask::getFromMetadata() +{ + m_index_dir.refresh(); + for (auto entry : m_index_dir.entryList(QDir::Files)) + { + auto metadata = Metadata::get(m_index_dir, entry); + + if (!metadata.isValid()) + continue; + + auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); + resource->setMetadata(metadata); + resource->setStatus(ResourceStatus::NOT_INSTALLED); + m_result->resources[resource->internal_id()].reset(resource); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.hpp new file mode 100644 index 0000000000..c798e003d3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.hpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include "minecraft/mod/Mod.hpp" +#include "tasks/Task.h" + +class ResourceFolderLoadTask : public Task +{ + Q_OBJECT + public: + struct Result + { + QMap resources; + }; + using ResultPtr = std::shared_ptr; + ResultPtr result() const + { + return m_result; + } + + public: + ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function create_function); + + bool canAbort() const override + { + return true; + } + bool abort() override + { + m_aborted.store(true); + return true; + } + + void executeTask() override; + + private: + void getFromMetadata(); + + private: + QDir m_resource_dir, m_index_dir; + bool m_is_indexed; + bool m_clean_orphan; + std::function m_create_func; + ResultPtr m_result; + + std::atomic m_aborted = false; + + /** This is the thread in which we should put new mod objects */ + QThread* m_thread_to_spawn_into; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeChange.cpp b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.cpp new file mode 100644 index 0000000000..cdba5e0a14 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "CapeChange.h" + +#include + +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +CapeChange::CapeChange(QString cape) : NetRequest(), m_capeId(cape) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* CapeChange::getReply(QNetworkRequest& request) +{ + if (m_capeId.isEmpty()) + { + setStatus(tr("Removing cape")); + return m_network->deleteResource(request); + } + else + { + setStatus(tr("Equipping cape")); + return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); + } +} + +CapeChange::Ptr CapeChange::make(QString token, QString capeId) +{ + auto up = makeShared(capeId); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeChange.h b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.h new file mode 100644 index 0000000000..c9170f973c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetRequest.h" + +class CapeChange : public Net::NetRequest +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + CapeChange(QString capeId); + virtual ~CapeChange() = default; + + static CapeChange::Ptr make(QString token, QString capeId); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_capeId; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.cpp b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.cpp new file mode 100644 index 0000000000..052fd67910 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.cpp @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "CapeListModel.h" + +#include +#include +#include + +#include "Application.h" +#include "FileSystem.h" +#include "net/Download.h" + +CapeListModel::CapeListModel(QObject* parent) : QAbstractListModel(parent) +{} + +void CapeListModel::loadFromAccount(MinecraftAccountPtr account, const QString& cacheDir) +{ + beginResetModel(); + + m_account = account; + m_cacheDir = cacheDir; + m_capes.clear(); + m_capeImages.clear(); + m_capeIndexMap.clear(); + + if (!m_account || !m_account->accountData()) + { + endResetModel(); + return; + } + + // Ensure cache directory exists + QDir().mkpath(m_cacheDir); + + auto& accountData = *m_account->accountData(); + + // Add "No Cape" option first + CapeInfo noCape; + noCape.id = "no_cape"; + noCape.alias = tr("No Cape"); + noCape.loaded = true; + m_capes.append(noCape); + m_capeIndexMap["no_cape"] = 0; + + // Add all available capes + int index = 1; + for (const auto& cape : accountData.minecraftProfile.capes) + { + CapeInfo info; + info.id = cape.id; + info.alias = cape.alias; + info.url = cape.url; + info.loaded = false; + + // Check if we have embedded data + if (!cape.data.isEmpty()) + { + QImage capeImage; + if (capeImage.loadFromData(cape.data, "PNG")) + { + m_capeImages[cape.id] = capeImage; + info.loaded = true; + + // Also save to cache + QString cachePath = FS::PathCombine(m_cacheDir, cape.id + ".png"); + capeImage.save(cachePath); + } + } + + // Try loading from cache if not embedded + if (!info.loaded) + { + loadCapeFromDisk(cape.id); + if (m_capeImages.contains(cape.id)) + { + info.loaded = true; + } + } + + m_capes.append(info); + m_capeIndexMap[cape.id] = index++; + } + + endResetModel(); + + // Download any missing capes + downloadCapes(); +} + +void CapeListModel::refresh() +{ + if (m_account) + { + loadFromAccount(m_account, m_cacheDir); + } +} + +void CapeListModel::loadCapeFromDisk(const QString& capeId) +{ + QString path = FS::PathCombine(m_cacheDir, capeId + ".png"); + if (QFileInfo::exists(path)) + { + QImage img; + if (img.load(path)) + { + m_capeImages[capeId] = img; + } + } +} + +void CapeListModel::downloadCapes() +{ + QList toDownload; + + for (int i = 0; i < m_capes.size(); ++i) + { + const auto& cape = m_capes[i]; + if (!cape.loaded && !cape.url.isEmpty() && !m_capeImages.contains(cape.id)) + { + toDownload.append(i); + } + } + + if (toDownload.isEmpty()) + { + emit loadingFinished(); + return; + } + + m_downloadJob.reset(new NetJob(tr("Download capes"), APPLICATION->network())); + + for (int idx : toDownload) + { + const auto& cape = m_capes[idx]; + QString cachePath = FS::PathCombine(m_cacheDir, cape.id + ".png"); + m_downloadJob->addNetAction(Net::Download::makeFile(cape.url, cachePath)); + } + + connect(m_downloadJob.get(), &NetJob::succeeded, this, &CapeListModel::onDownloadSucceeded); + connect(m_downloadJob.get(), &NetJob::failed, this, &CapeListModel::onDownloadFailed); + + m_downloadJob->start(); +} + +void CapeListModel::onDownloadSucceeded() +{ + m_downloadJob.reset(); + + // Load all downloaded capes + for (auto& cape : m_capes) + { + if (!cape.loaded && !cape.id.isEmpty() && cape.id != "no_cape") + { + loadCapeFromDisk(cape.id); + if (m_capeImages.contains(cape.id)) + { + cape.loaded = true; + int idx = m_capeIndexMap.value(cape.id, -1); + if (idx >= 0) + { + emit dataChanged(index(idx), index(idx)); + emit capeLoaded(cape.id); + } + } + } + } + + emit loadingFinished(); +} + +void CapeListModel::onDownloadFailed(QString reason) +{ + qWarning() << "Cape download failed:" << reason; + m_downloadJob.reset(); + emit loadingFinished(); +} + +QImage CapeListModel::getCapeImage(const QString& capeId) const +{ + return m_capeImages.value(capeId, QImage()); +} + +int CapeListModel::findCapeIndex(const QString& capeId) const +{ + QString id = capeId; + if (id.isEmpty()) + { + id = "no_cape"; + } + return m_capeIndexMap.value(id, -1); +} + +QString CapeListModel::capeIdAt(int index) const +{ + if (index < 0 || index >= m_capes.size()) + return QString(); + return m_capes[index].id; +} + +int CapeListModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return m_capes.size(); +} + +QVariant CapeListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_capes.size()) + return QVariant(); + + const auto& cape = m_capes[index.row()]; + + switch (role) + { + case Qt::DisplayRole: return cape.alias; + + case Qt::DecorationRole: + { + QImage img = m_capeImages.value(cape.id, QImage()); + if (!img.isNull()) + { + return createCapePreview(img, m_elytraMode); + } + return QVariant(); + } + + case CapeIdRole: return cape.id; + + case CapeAliasRole: return cape.alias; + + case CapeImageRole: return m_capeImages.value(cape.id, QImage()); + + case CapeUrlRole: return cape.url; + + default: return QVariant(); + } +} + +QHash CapeListModel::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles[CapeIdRole] = "capeId"; + roles[CapeAliasRole] = "capeAlias"; + roles[CapeImageRole] = "capeImage"; + roles[CapeUrlRole] = "capeUrl"; + return roles; +} + +QPixmap CapeListModel::createCapePreview(const QImage& capeImage, bool elytra) const +{ + if (elytra) + { + auto wing = capeImage.copy(34, 0, 12, 22); + QImage mirrored = wing.flipped(Qt::Horizontal); + + QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format()); + combined.fill(Qt::transparent); + + QPainter painter(&combined); + painter.drawImage(0, 0, wing); + painter.drawImage(wing.width() - 2, 0, mirrored); + painter.end(); + return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation)); + } + return QPixmap::fromImage( + capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.h b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.h new file mode 100644 index 0000000000..ed9b2f4ebb --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.h @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "minecraft/auth/MinecraftAccount.hpp" +#include "net/NetJob.h" + +/** + * @brief Model for managing Minecraft capes with on-demand loading + * + * This model provides a list of available capes for a Minecraft account, + * downloading cape images on demand and caching them locally. + */ +class CapeListModel : public QAbstractListModel +{ + Q_OBJECT + + public: + enum Roles + { + CapeIdRole = Qt::UserRole, + CapeAliasRole, + CapeImageRole, + CapeUrlRole + }; + + explicit CapeListModel(QObject* parent = nullptr); + virtual ~CapeListModel() = default; + + /** + * @brief Initialize the model with account data + * @param account The Minecraft account to load capes from + * @param cacheDir Directory to cache cape images + */ + void loadFromAccount(MinecraftAccountPtr account, const QString& cacheDir); + + /** + * @brief Refresh cape data from the account + */ + void refresh(); + + /** + * @brief Get cape image by ID + * @param capeId The cape identifier + * @return Cape image or null image if not loaded + */ + QImage getCapeImage(const QString& capeId) const; + + /** + * @brief Get all loaded cape images + * @return Hash of cape ID to image + */ + QHash allCapes() const + { + return m_capeImages; + } + + /** + * @brief Find index of cape by ID + * @param capeId The cape identifier + * @return Index in model or -1 if not found + */ + int findCapeIndex(const QString& capeId) const; + + /** + * @brief Get cape ID at index + * @param index Model index + * @return Cape ID or empty string + */ + QString capeIdAt(int index) const; + + // QAbstractListModel interface + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + signals: + /** + * @brief Emitted when a cape image has been loaded + * @param capeId The cape that was loaded + */ + void capeLoaded(const QString& capeId); + + /** + * @brief Emitted when all capes have finished loading + */ + void loadingFinished(); + + private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + + private: + struct CapeInfo + { + QString id; + QString alias; + QString url; + bool loaded = false; + }; + + void downloadCapes(); + void loadCapeFromDisk(const QString& capeId); + QPixmap createCapePreview(const QImage& capeImage, bool elytra = false) const; + + MinecraftAccountPtr m_account; + QString m_cacheDir; + QList m_capes; + QHash m_capeImages; + QHash m_capeIndexMap; + NetJob::Ptr m_downloadJob; + bool m_elytraMode = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.cpp new file mode 100644 index 0000000000..5675eb6d6f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ +#include "SkinDelete.h" + +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +SkinDelete::SkinDelete() : NetRequest() +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) +{ + setStatus(tr("Deleting skin")); + return m_network->deleteResource(request); +} + +SkinDelete::Ptr SkinDelete::make(QString token) +{ + auto up = makeShared(); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.h b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.h new file mode 100644 index 0000000000..80d2bb5417 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinDelete : public Net::NetRequest +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + SkinDelete(); + virtual ~SkinDelete() = default; + + static SkinDelete::Ptr make(QString token); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinList.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinList.cpp new file mode 100644 index 0000000000..522cee2e78 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinList.cpp @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "SkinList.h" + +#include +#include + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/skins/SkinModel.h" + +SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher.reset(new QFileSystemWatcher(this)); + m_isWatching = false; + connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); + connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); + directoryChanged(path); +} + +void SkinList::startWatching() +{ + if (m_isWatching) + { + return; + } + update(); + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void SkinList::stopWatching() +{ + save(); + if (!m_isWatching) + { + return; + } + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool SkinList::update() +{ + QList newSkins; + m_dir.refresh(); + + auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); + if (manifestInfo.exists()) + { + try + { + auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); + const auto root = doc.object(); + auto skins = Json::ensureArray(root, "skins"); + for (auto jSkin : skins) + { + SkinModel s(m_dir, Json::ensureObject(jSkin)); + if (s.isValid()) + { + newSkins << s; + } + } + } + catch (const Exception& e) + { + qCritical() << "Couldn't load skins json:" << e.cause(); + } + } + + bool needsSave = false; + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + if (!skin.url.isEmpty() && !skin.data.isEmpty()) + { + QPixmap skinTexture; + SkinModel* nskin = nullptr; + for (auto i = 0; i < newSkins.size(); i++) + { + if (newSkins[i].getURL() == skin.url) + { + nskin = &newSkins[i]; + break; + } + } + if (!nskin) + { + auto name = m_acct->profileName() + ".png"; + if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) + { + name = QUrl(skin.url).fileName() + ".png"; + } + auto path = m_dir.absoluteFilePath(name); + if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) + { + SkinModel s(path); + s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + s.setURL(skin.url); + newSkins << s; + needsSave = true; + } + } + else + { + nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + } + } + + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) + { + if (!entry.isFile() && entry.suffix() != "png") + continue; + + SkinModel w(entry.absoluteFilePath()); + if (w.isValid()) + { + auto add = true; + for (auto s : newSkins) + { + if (s.name() == w.name()) + { + add = false; + break; + } + } + if (add) + { + newSkins.append(w); + needsSave = true; + } + } + } + std::sort(newSkins.begin(), + newSkins.end(), + [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); + beginResetModel(); + m_skinList.swap(newSkins); + endResetModel(); + if (needsSave) + save(); + return true; +} + +void SkinList::directoryChanged(const QString& path) +{ + QDir new_dir(path); + if (!new_dir.exists()) + if (!FS::ensureFolderPathExists(new_dir.absolutePath())) + return; + if (m_dir.absolutePath() != new_dir.absolutePath()) + { + m_dir.setPath(path); + m_dir.refresh(); + if (m_isWatching) + stopWatching(); + startWatching(); + } + update(); +} + +void SkinList::fileChanged(const QString& path) +{ + qDebug() << "Checking " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + return; + + for (int i = 0; i < m_skinList.count(); i++) + { + if (m_skinList[i].getPath() == checkfile.absoluteFilePath()) + { + m_skinList[i].refresh(); + dataChanged(index(i), index(i)); + break; + } + } +} + +QStringList SkinList::mimeTypes() const +{ + return { "text/uri-list" }; +} + +Qt::DropActions SkinList::supportedDropActions() const +{ + return Qt::CopyAction; +} + +bool SkinList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + QStringList skinFiles; + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + skinFiles << url.toLocalFile(); + } + installSkins(skinFiles); + return true; + } + return false; +} + +Qt::ItemFlags SkinList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index); + if (index.isValid()) + { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + return f; +} + +QVariant SkinList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + + if (row < 0 || row >= m_skinList.size()) + return QVariant(); + auto skin = m_skinList[row]; + switch (role) + { + case Qt::DecorationRole: + { + auto preview = skin.getPreview(); + if (preview.isNull()) + { + preview = skin.getTexture(); + } + return preview; + } + case Qt::DisplayRole: return skin.name(); + case Qt::UserRole: return skin.name(); + case Qt::EditRole: return skin.name(); + default: return QVariant(); + } +} + +int SkinList::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_skinList.size(); +} + +void SkinList::installSkins(const QStringList& iconFiles) +{ + for (QString file : iconFiles) + installSkin(file); +} + +QString getUniqueFile(const QString& root, const QString& file) +{ + auto result = FS::PathCombine(root, file); + if (!QFileInfo::exists(result)) + { + return result; + } + + QString baseName = QFileInfo(file).completeBaseName(); + QString extension = QFileInfo(file).suffix(); + int tries = 0; + while (QFileInfo::exists(result)) + { + if (++tries > 256) + return {}; + + QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension); + result = FS::PathCombine(root, key); + } + + return result; +} +QString SkinList::installSkin(const QString& file, const QString& name) +{ + if (file.isEmpty()) + return tr("Path is empty."); + QFileInfo fileinfo(file); + if (!fileinfo.exists()) + return tr("File doesn't exist."); + if (!fileinfo.isFile()) + return tr("Not a file."); + if (!fileinfo.isReadable()) + return tr("File is not readable."); + if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) + return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); + + QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); + + return QFile::copy(file, target) ? "" : tr("Unable to copy file"); +} + +int SkinList::getSkinIndex(const QString& key) const +{ + for (int i = 0; i < m_skinList.count(); i++) + { + if (m_skinList[i].name() == key) + { + return i; + } + } + return -1; +} + +const SkinModel* SkinList::skin(const QString& key) const +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skinList[idx]; +} + +SkinModel* SkinList::skin(const QString& key) +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skinList[idx]; +} + +bool SkinList::deleteSkin(const QString& key, bool trash) +{ + int idx = getSkinIndex(key); + if (idx != -1) + { + auto s = m_skinList[idx]; + if (trash) + { + if (FS::trash(s.getPath(), nullptr)) + { + m_skinList.remove(idx); + save(); + return true; + } + } + else if (QFile::remove(s.getPath())) + { + m_skinList.remove(idx); + save(); + return true; + } + } + return false; +} + +void SkinList::save() +{ + QJsonObject doc; + QJsonArray arr; + for (auto s : m_skinList) + { + arr << s.toJSON(); + } + doc["skins"] = arr; + try + { + Json::write(doc, m_dir.absoluteFilePath("index.json")); + } + catch (const FS::FileSystemException& e) + { + qCritical() << "Failed to write skin index file :" << e.cause(); + } +} + +int SkinList::getSelectedAccountSkin() +{ + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + for (int i = 0; i < m_skinList.count(); i++) + { + if (m_skinList[i].getURL() == skin.url) + { + return i; + } + } + return -1; +} + +bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (!idx.isValid() || role != Qt::EditRole) + { + return false; + } + + int row = idx.row(); + if (row < 0 || row >= m_skinList.size()) + return false; + auto& skin = m_skinList[row]; + auto newName = value.toString(); + if (skin.name() != newName) + { + if (!skin.rename(newName)) + return false; + save(); + } + return true; +} + +void SkinList::updateSkin(SkinModel* s) +{ + auto done = false; + for (auto i = 0; i < m_skinList.size(); i++) + { + if (m_skinList[i].getPath() == s->getPath()) + { + m_skinList[i].setCapeId(s->getCapeId()); + m_skinList[i].setModel(s->getModel()); + m_skinList[i].setURL(s->getURL()); + done = true; + break; + } + } + if (!done) + { + beginInsertRows(QModelIndex(), m_skinList.count(), m_skinList.count() + 1); + m_skinList.append(*s); + endInsertRows(); + } + save(); +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinList.h b/archived/projt-launcher/launcher/minecraft/skins/SkinList.h new file mode 100644 index 0000000000..9e5c041f73 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinList.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" +#include "SkinModel.h" +#include "minecraft/auth/MinecraftAccount.hpp" + +class SkinList : public QAbstractListModel +{ + Q_OBJECT + public: + explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct); + virtual ~SkinList() + { + save(); + }; + + int getSkinIndex(const QString& key) const; + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& idx, const QVariant& value, int role) override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + virtual QStringList mimeTypes() const override; + virtual Qt::DropActions supportedDropActions() const override; + virtual bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + + bool deleteSkin(const QString& key, bool trash); + + void installSkins(const QStringList& iconFiles); + QString installSkin(const QString& file, const QString& name = {}); + + const SkinModel* skin(const QString& key) const; + SkinModel* skin(const QString& key); + + void startWatching(); + void stopWatching(); + + QString getDir() const + { + return m_dir.absolutePath(); + } + void save(); + int getSelectedAccountSkin(); + + void updateSkin(SkinModel* s); + + private: + // hide copy constructor + SkinList(const SkinList&) = delete; + // hide assign op + SkinList& operator=(const SkinList&) = delete; + + protected slots: + void directoryChanged(const QString& path); + void fileChanged(const QString& path); + bool update(); + + private: + shared_qobject_ptr m_watcher; + bool m_isWatching; + QList m_skinList; + QDir m_dir; + MinecraftAccountPtr m_acct; +}; \ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinModel.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.cpp new file mode 100644 index 0000000000..875399197f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.cpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "SkinModel.h" +#include +#include + +#include "FileSystem.h" +#include "Json.h" + +static QImage improveSkin(QImage skin) +{ + int height = skin.height(); + int width = skin.width(); + if (width != 64 || (height != 32 && height != 64)) + { + return skin; + } + + // It seems some older skins may use this format, which can't be drawn onto + // https://github.com/PrismLauncher/PrismLauncher/issues/4032 + // https://doc.qt.io/qt-6/qpainter.html#begin + if (skin.format() <= QImage::Format_Indexed8 || !skin.hasAlphaChannel()) + { + skin = skin.convertToFormat(QImage::Format_ARGB32); + } + if (skin.size() == QSize(64, 32)) + { + // old format + auto newSkin = QImage(QSize(64, 64), skin.format()); + newSkin.fill(Qt::transparent); + QPainter p(&newSkin); + p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head + + auto leg = skin.copy(QRect(0, 16, 16, 16)); + p.drawImage(QPoint(16, 48), leg); // copy leg + + auto arm = skin.copy(QRect(40, 16, 16, 16)); + p.drawImage(QPoint(32, 48), arm); // copy arm + return newSkin; + } + return skin; +} +static QImage getSkin(const QString path) +{ + return improveSkin(QImage(path)); +} + +static QImage generatePreviews(QImage texture, bool slim) +{ + QImage preview(36, 36, QImage::Format_ARGB32); + preview.fill(Qt::transparent); + QPainter paint(&preview); + + // head + paint.drawImage(4, 2, texture.copy(8, 8, 8, 8)); + paint.drawImage(4, 2, texture.copy(40, 8, 8, 8)); + // torso + paint.drawImage(4, 10, texture.copy(20, 20, 8, 12)); + paint.drawImage(4, 10, texture.copy(20, 36, 8, 12)); + // right leg + paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); + paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); + // left leg + paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); + paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); + + auto armWidth = slim ? 3 : 4; + auto armPosX = slim ? 1 : 0; + // right arm + paint.drawImage(armPosX, 10, texture.copy(44, 20, armWidth, 12)); + paint.drawImage(armPosX, 10, texture.copy(44, 36, armWidth, 12)); + // left arm + paint.drawImage(12, 10, texture.copy(36, 52, armWidth, 12)); + paint.drawImage(12, 10, texture.copy(52, 52, armWidth, 12)); + + // back + // head + paint.drawImage(24, 2, texture.copy(24, 8, 8, 8)); + paint.drawImage(24, 2, texture.copy(56, 8, 8, 8)); + // torso + paint.drawImage(24, 10, texture.copy(32, 20, 8, 12)); + paint.drawImage(24, 10, texture.copy(32, 36, 8, 12)); + // right leg + paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); + paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); + // left leg + paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); + paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); + + // right arm + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 36, armWidth, 12)); + // left arm + paint.drawImage(32, 10, texture.copy(40 + armWidth, 52, armWidth, 12)); + paint.drawImage(32, 10, texture.copy(56 + armWidth, 52, armWidth, 12)); + + return preview; +} +SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_model(Model::CLASSIC) +{ + m_preview = generatePreviews(m_texture, false); +} + +SkinModel::SkinModel(QDir skinDir, QJsonObject obj) + : m_capeId(Json::ensureString(obj, "capeId")), + m_model(Model::CLASSIC), + m_url(Json::ensureString(obj, "url")) +{ + auto name = Json::ensureString(obj, "name"); + + if (auto model = Json::ensureString(obj, "model"); model == "SLIM") + { + m_model = Model::SLIM; + } + m_path = skinDir.absoluteFilePath(name) + ".png"; + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} + +QString SkinModel::name() const +{ + return QFileInfo(m_path).completeBaseName(); +} + +bool SkinModel::rename(QString newName) +{ + auto info = QFileInfo(m_path); + auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + if (QFileInfo::exists(new_path)) + { + return false; + } + m_path = new_path; + return FS::move(info.absoluteFilePath(), m_path); +} + +QJsonObject SkinModel::toJSON() const +{ + QJsonObject obj; + obj["name"] = name(); + obj["capeId"] = m_capeId; + obj["url"] = m_url; + obj["model"] = getModelString(); + return obj; +} + +QString SkinModel::getModelString() const +{ + switch (m_model) + { + case CLASSIC: return "CLASSIC"; + case SLIM: return "SLIM"; + } + return {}; +} + +bool SkinModel::isValid() const +{ + return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) + && m_texture.size().width() == 64; +} +void SkinModel::refresh() +{ + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} +void SkinModel::setModel(Model model) +{ + m_model = model; + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinModel.h b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.h new file mode 100644 index 0000000000..09b711c1a4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +class SkinModel +{ + public: + enum Model + { + CLASSIC, + SLIM + }; + + SkinModel() = default; + SkinModel(QString path); + SkinModel(QDir skinDir, QJsonObject obj); + virtual ~SkinModel() = default; + + QString name() const; + QString getModelString() const; + bool isValid() const; + QString getPath() const + { + return m_path; + } + QImage getTexture() const + { + return m_texture; + } + QImage getPreview() const + { + return m_preview; + } + QString getCapeId() const + { + return m_capeId; + } + Model getModel() const + { + return m_model; + } + QString getURL() const + { + return m_url; + } + + bool rename(QString newName); + void setCapeId(QString capeID) + { + m_capeId = capeID; + } + void setModel(Model model); + void setURL(QString url) + { + m_url = url; + } + void refresh(); + + QJsonObject toJSON() const; + + private: + QString m_path; + QImage m_texture; + QImage m_preview; + QString m_capeId; + Model m_model; + QString m_url; +}; \ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.cpp new file mode 100644 index 0000000000..93d77dbeee --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "SkinUpload.h" + +#include + +#include "FileSystem.h" +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +SkinUpload::SkinUpload(QString path, QString variant) : NetRequest(), m_path(path), m_variant(variant) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) +{ + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); + + QHttpPart skin; + skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + + skin.setBody(FS::read(m_path)); + + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(m_variant.toUtf8()); + + multiPart->append(skin); + multiPart->append(model); + setStatus(tr("Uploading skin")); + return m_network->post(request, multiPart); +} + +SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) +{ + auto up = makeShared(path, variant); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.h b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.h new file mode 100644 index 0000000000..f39db0a59e --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinUpload : public Net::NetRequest +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + // Note this class takes ownership of the file. + SkinUpload(QString path, QString variant); + virtual ~SkinUpload() = default; + + static SkinUpload::Ptr make(QString token, QString path, QString variant); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_path; + QString m_variant; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.cpp b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.cpp new file mode 100644 index 0000000000..2e205f3763 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "AssetUpdateTask.h" + +#include "BuildConfig.h" +#include "launch/LaunchStage.hpp" +#include "minecraft/AssetsUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/ChecksumValidator.h" + +#include "Application.h" + +#include "net/ApiDownload.h" + +AssetUpdateTask::AssetUpdateTask(MinecraftInstance* inst) +{ + m_inst = inst; +} + +void AssetUpdateTask::executeTask() +{ + setStatus(tr("Updating assets index...")); + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + QUrl indexUrl = assets->url; + QString localPath = assets->id + ".json"; + auto job = makeShared(tr("Asset index for %1").arg(m_inst->name()), APPLICATION->network()); + + auto metacache = APPLICATION->metacache(); + auto entry = metacache->resolveEntry("asset_indexes", localPath); + entry->setStale(true); + auto hexSha1 = assets->sha1.toLatin1(); + qDebug() << "Asset index SHA1:" << hexSha1; + auto dl = Net::ApiDownload::makeCached(indexUrl, entry); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, assets->sha1)); + job->addNetAction(dl); + + downloadJob.reset(job); + + connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished); + connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); + connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); + connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); + + qDebug() << m_inst->name() << ": Starting asset index download"; + downloadJob->start(); +} + +bool AssetUpdateTask::canAbort() const +{ + return true; +} + +void AssetUpdateTask::assetIndexFinished() +{ + AssetsIndex index; + qDebug() << m_inst->name() << ": Finished asset index download"; + + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + QString asset_fname = "assets/indexes/" + assets->id + ".json"; + // NOTE: Current validation is done via AssetsUtils::loadAssetsIndexJson. + // Future improvement: Implement a generic validator based on JSON schema for more robust checks. + if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) + { + auto metacache = APPLICATION->metacache(); + auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); + metacache->evictEntry(entry); + emitFailed(tr("Failed to read the assets index!")); + } + + auto job = index.getDownloadJob(); + if (job) + { + QString resourceURLStr = APPLICATION->settings()->get("ResourceURL").toString(); + QString source = tr("Mojang"); + if (!resourceURLStr.isEmpty() && resourceURLStr != BuildConfig.DEFAULT_RESOURCE_BASE) + { + source = QUrl(resourceURLStr).host(); + } + setStatus(tr("Getting the asset files from %1...").arg(source)); + downloadJob = job; + connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); + connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); + connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); + connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); + downloadJob->start(); + return; + } + emitSucceeded(); +} + +void AssetUpdateTask::assetIndexFailed(QString reason) +{ + qDebug() << m_inst->name() << ": Failed asset index download"; + emitFailed(tr("Failed to download the assets index:\n%1").arg(reason)); +} + +void AssetUpdateTask::assetsFailed(QString reason) +{ + emitFailed(tr("Failed to download assets:\n%1").arg(reason)); +} + +bool AssetUpdateTask::abort() +{ + if (downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted AssetUpdateTask"; + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.h b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.h new file mode 100644 index 0000000000..65c9263a47 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include "net/NetJob.h" +#include "tasks/Task.h" +class MinecraftInstance; + +class AssetUpdateTask : public Task +{ + Q_OBJECT + public: + AssetUpdateTask(MinecraftInstance* inst); + virtual ~AssetUpdateTask() = default; + + void executeTask() override; + + bool canAbort() const override; + + private slots: + void assetIndexFinished(); + void assetIndexFailed(QString reason); + void assetsFailed(QString reason); + + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst; + NetJob::Ptr downloadJob; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.cpp b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.cpp new file mode 100644 index 0000000000..5aa2c6dd36 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "FMLLibrariesTask.h" + +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFilterData.h" + +#include "Application.h" +#include "BuildConfig.h" + +#include "net/ApiDownload.h" + +FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance* inst) +{ + m_inst = inst; +} +void FMLLibrariesTask::executeTask() +{ + // Get the mod list + MinecraftInstance* inst = (MinecraftInstance*)m_inst; + auto components = inst->getPackProfile(); + auto profile = components->getProfile(); + + if (!profile->hasTrait("legacyFML")) + { + emitSucceeded(); + return; + } + + QString version = components->getComponentVersion("net.minecraft"); + auto& fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; + if (!fmlLibsMapping.contains(version)) + { + emitSucceeded(); + return; + } + + auto& libList = fmlLibsMapping[version]; + + // determine if we need some libs for FML or forge + setStatus(tr("Checking for FML libraries...")); + if (!components->getComponent("net.minecraftforge")) + { + emitSucceeded(); + return; + } + + // now check the lib folder inside the instance for files. + for (auto& lib : libList) + { + QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); + if (libInfo.exists()) + continue; + fmlLibsToProcess.append(lib); + } + + // if everything is in place, there's nothing to do here... + if (fmlLibsToProcess.isEmpty()) + { + emitSucceeded(); + return; + } + + // download missing libs to our place + setStatus(tr("Downloading FML libraries...")); + NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) }; + auto metacache = APPLICATION->metacache(); + Net::Download::Options options = Net::Download::Option::MakeEternal; + for (auto& lib : fmlLibsToProcess) + { + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; + dljob->addNetAction(Net::ApiDownload::makeCached(QUrl(urlString), entry, options)); + } + + connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); + connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); + connect(dljob.get(), &NetJob::aborted, this, &FMLLibrariesTask::emitAborted); + connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress); + connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propagateStepProgress); + downloadJob.reset(dljob); + downloadJob->start(); +} + +bool FMLLibrariesTask::canAbort() const +{ + return true; +} + +void FMLLibrariesTask::fmllibsFinished() +{ + downloadJob.reset(); + if (!fmlLibsToProcess.isEmpty()) + { + setStatus(tr("Copying FML libraries into the instance...")); + MinecraftInstance* inst = (MinecraftInstance*)m_inst; + auto metacache = APPLICATION->metacache(); + int index = 0; + for (auto& lib : fmlLibsToProcess) + { + progress(index, fmlLibsToProcess.size()); + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + auto path = FS::PathCombine(inst->libDir(), lib.filename); + if (!FS::ensureFilePathExists(path)) + { + emitFailed(tr("Failed creating FML library folder inside the instance.")); + return; + } + if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) + { + emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); + return; + } + index++; + } + progress(index, fmlLibsToProcess.size()); + } + emitSucceeded(); +} +void FMLLibrariesTask::fmllibsFailed(QString reason) +{ + QStringList failed = downloadJob->getFailedFiles(); + QString failed_all = failed.join("\n"); + emitFailed( + tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); +} + +bool FMLLibrariesTask::abort() +{ + if (downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted FMLLibrariesTask"; + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.h b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.h new file mode 100644 index 0000000000..b96db03c1c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include "minecraft/VersionFilterData.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +class MinecraftInstance; + +class FMLLibrariesTask : public Task +{ + Q_OBJECT + public: + FMLLibrariesTask(MinecraftInstance* inst); + virtual ~FMLLibrariesTask() = default; + + void executeTask() override; + + bool canAbort() const override; + + private slots: + void fmllibsFinished(); + void fmllibsFailed(QString reason); + + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst; + NetJob::Ptr downloadJob; + QList fmlLibsToProcess; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/FoldersTask.cpp b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.cpp new file mode 100644 index 0000000000..7177ed22d2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "FoldersTask.h" +#include +#include "minecraft/MinecraftInstance.h" + +FoldersTask::FoldersTask(MinecraftInstance* inst) +{ + m_inst = inst; +} + +void FoldersTask::executeTask() +{ + // Make directories + QDir mcDir(m_inst->gameRoot()); + if (!mcDir.exists() && !mcDir.mkpath(".")) + { + emitFailed(tr("Failed to create folder for Minecraft binaries.")); + return; + } + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/update/FoldersTask.h b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.h new file mode 100644 index 0000000000..426c162d78 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "tasks/Task.h" + +class MinecraftInstance; +class FoldersTask : public Task +{ + Q_OBJECT + public: + FoldersTask(MinecraftInstance* inst); + virtual ~FoldersTask() = default; + + void executeTask() override; + + private: + MinecraftInstance* m_inst; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.cpp b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.cpp new file mode 100644 index 0000000000..6fa5796d0f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.cpp @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "LibrariesTask.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "Application.h" + +LibrariesTask::LibrariesTask(MinecraftInstance* inst) +{ + m_inst = inst; +} + +void LibrariesTask::executeTask() +{ + setStatus(tr("Downloading required library files...")); + qDebug() << m_inst->name() << ": downloading libraries"; + MinecraftInstance* inst = (MinecraftInstance*)m_inst; + + // Build a list of URLs that will need to be downloaded. + auto components = inst->getPackProfile(); + auto profile = components->getProfile(); + + NetJob::Ptr job{ new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()) }; + downloadJob.reset(job); + + auto metacache = APPLICATION->metacache(); + + auto processArtifactPool = + [this, inst, metacache](const QList& pool, QStringList& errors, const QString& localPath) + { + for (auto lib : pool) + { + if (!lib) + { + emitFailed(tr("Null jar is specified in the metadata, aborting.")); + return false; + } + auto dls = lib->getDownloads(inst->runtimeContext(), metacache.get(), errors, localPath); + for (auto dl : dls) + { + downloadJob->addNetAction(dl); + } + } + return true; + }; + + QStringList failedLocalLibraries; + QList libArtifactPool; + libArtifactPool.append(profile->getLibraries()); + libArtifactPool.append(profile->getNativeLibraries()); + libArtifactPool.append(profile->getMavenFiles()); + for (auto agent : profile->getAgents()) + { + libArtifactPool.append(agent->library()); + } + libArtifactPool.append(profile->getMainJar()); + processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); + + QStringList failedLocalJarMods; + processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir()); + + if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty()) + { + downloadJob.reset(); + QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n"); + emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the " + "files, or removed the " + "packages that require them.\nYou'll have to correct this problem manually.") + .arg(failed_all)); + return; + } + + connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); + connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); + connect(downloadJob.get(), &NetJob::aborted, this, &LibrariesTask::emitAborted); + connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propagateStepProgress); + + downloadJob->start(); +} + +bool LibrariesTask::canAbort() const +{ + return true; +} + +void LibrariesTask::jarlibFailed(QString reason) +{ + emitFailed(tr("Game update failed: it was impossible to fetch the required libraries.\nReason:\n%1").arg(reason)); +} + +bool LibrariesTask::abort() +{ + if (downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted LibrariesTask"; + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.h b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.h new file mode 100644 index 0000000000..7c4c9c54a0 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include "net/NetJob.h" +#include "tasks/Task.h" +class MinecraftInstance; + +class LibrariesTask : public Task +{ + Q_OBJECT + public: + LibrariesTask(MinecraftInstance* inst); + virtual ~LibrariesTask() = default; + + void executeTask() override; + + bool canAbort() const override; + + private slots: + void jarlibFailed(QString reason); + + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst; + NetJob::Ptr downloadJob; +}; diff --git a/archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h b/archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h new file mode 100644 index 0000000000..81ef90b9a6 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/CheckUpdateTask.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "minecraft/mod/tasks/GetModDependenciesTask.hpp" +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class ResourceDownloadTask; +class ModFolderModel; + +class CheckUpdateTask : public Task +{ + Q_OBJECT + + public: + CheckUpdateTask(QList& resources, + std::list& mcVersions, + QList loadersList, + std::shared_ptr resourceModel) + : Task(), + m_resources(resources), + m_gameVersions(mcVersions), + m_loadersList(std::move(loadersList)), + m_resourceModel(std::move(resourceModel)) + {} + + struct Update + { + QString name; + QString old_hash; + QString old_version; + QString new_version; + std::optional new_version_type; + QString changelog; + ModPlatform::ResourceProvider provider; + shared_qobject_ptr download; + bool enabled = true; + + public: + Update(QString name, + QString old_h, + QString old_v, + QString new_v, + std::optional new_v_type, + QString changelog, + ModPlatform::ResourceProvider p, + shared_qobject_ptr t, + bool enabled = true) + : name(std::move(name)), + old_hash(std::move(old_h)), + old_version(std::move(old_v)), + new_version(std::move(new_v)), + new_version_type(std::move(new_v_type)), + changelog(std::move(changelog)), + provider(p), + download(std::move(t)), + enabled(enabled) + {} + }; + + auto getUpdates() -> std::vector&& + { + return std::move(m_updates); + } + auto getDependencies() -> QList>&& + { + return std::move(m_deps); + } + + public slots: + bool abort() override = 0; + + protected slots: + void executeTask() override = 0; + + signals: + void checkFailed(Resource* failed, QString reason, QUrl recover_url = {}); + + protected: + QList& m_resources; + std::list& m_gameVersions; + QList m_loadersList; + std::shared_ptr m_resourceModel; + + std::vector m_updates; + QList> m_deps; +}; diff --git a/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp new file mode 100644 index 0000000000..460e9473ed --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.cpp @@ -0,0 +1,668 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "EnsureMetadataTask.h" + +#include +#include + +#include "Application.h" +#include "Json.h" + +#include "QObjectPtr.h" +#include "minecraft/mod/Mod.hpp" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.hpp" + +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/helpers/HashUtils.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" + +static ModrinthAPI modrinth_api; +static FlameAPI flame_api; + +EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), + m_indexDir(dir), + m_provider(prov), + m_hashingTask(nullptr), + m_currentTask(nullptr) +{ + auto hashTask = createNewHash(resource); + if (!hashTask) + return; + connect(hashTask.get(), + &Hashing::Hasher::resultsReady, + [this, resource](QString hash) { m_resources.insert(hash, resource); }); + connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); + m_hashingTask = hashTask; +} + +EnsureMetadataTask::EnsureMetadataTask(QList& resources, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), + m_indexDir(dir), + m_provider(prov), + m_currentTask(nullptr) +{ + auto hashTask = + makeShared("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + m_hashingTask = hashTask; + for (auto* resource : resources) + { + auto hash_task = createNewHash(resource); + if (!hash_task) + continue; + connect(hash_task.get(), + &Hashing::Hasher::resultsReady, + [this, resource](QString hash) { m_resources.insert(hash, resource); }); + connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); + hashTask->addTask(hash_task); + } +} + +EnsureMetadataTask::EnsureMetadataTask(QHash& resources, + QDir dir, + ModPlatform::ResourceProvider prov) + : Task(), + m_resources(resources), + m_indexDir(dir), + m_provider(prov), + m_currentTask(nullptr) +{} + +Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource) +{ + if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER) + return nullptr; + + return Hashing::createHasher(resource->fileinfo().absoluteFilePath(), m_provider); +} + +QString EnsureMetadataTask::getExistingHash(Resource* resource) +{ + // Check for already computed hashes + // (linear on the number of mods vs. linear on the size of the mod's JAR) + auto it = m_resources.keyValueBegin(); + while (it != m_resources.keyValueEnd()) + { + if ((*it).second == resource) + break; + it++; + } + + // We already have the hash computed + if (it != m_resources.keyValueEnd()) + { + return (*it).first; + } + + // No existing hash + return {}; +} + +bool EnsureMetadataTask::abort() +{ + // Prevent sending signals to a dead object + disconnect(this, 0, 0, 0); + + if (m_currentTask) + return m_currentTask->abort(); + return true; +} + +void EnsureMetadataTask::executeTask() +{ + setStatus(tr("Checking if resources have metadata...")); + + for (auto* resource : m_resources) + { + if (!resource->valid()) + { + qDebug() << "Resource" << resource->name() << "is invalid!"; + emitFail(resource); + continue; + } + + // They already have the right metadata :o + if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata() + && resource->metadata()->provider == m_provider) + { + qDebug() << "Resource" << resource->name() << "already has metadata!"; + emitReady(resource); + continue; + } + + // Folders don't have metadata + if (resource->type() == ResourceType::FOLDER) + { + emitReady(resource); + } + } + + Task::Ptr version_task; + + switch (m_provider) + { + case (ModPlatform::ResourceProvider::MODRINTH): version_task = modrinthVersionsTask(); break; + case (ModPlatform::ResourceProvider::FLAME): version_task = flameVersionsTask(); break; + } + + auto invalidade_leftover = [this] + { + for (auto resource = m_resources.constBegin(); resource != m_resources.constEnd(); resource++) + emitFail(resource.value(), resource.key(), RemoveFromList::No); + m_resources.clear(); + + emitSucceeded(); + }; + + connect(version_task.get(), + &Task::finished, + this, + [this, invalidade_leftover] + { + Task::Ptr project_task; + + switch (m_provider) + { + case (ModPlatform::ResourceProvider::MODRINTH): project_task = modrinthProjectsTask(); break; + case (ModPlatform::ResourceProvider::FLAME): project_task = flameProjectsTask(); break; + } + + if (!project_task) + { + invalidade_leftover(); + return; + } + + connect(project_task.get(), + &Task::finished, + this, + [this, invalidade_leftover, project_task] + { + invalidade_leftover(); + project_task->deleteLater(); + if (m_currentTask) + m_currentTask.reset(); + }); + connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed); + + m_currentTask = project_task; + project_task->start(); + }); + + if (m_resources.size() > 1) + setStatus(tr("Requesting metadata information from %1...") + .arg(ModPlatform::ProviderCapabilities::readableName(m_provider))); + else if (!m_resources.empty()) + setStatus( + tr("Requesting metadata information from %1 for '%2'...") + .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name())); + + m_currentTask = version_task; + version_task->start(); +} + +void EnsureMetadataTask::emitReady(Resource* resource, QString key, RemoveFromList remove) +{ + if (!resource) + { + qCritical() << "Tried to mark a null resource as ready."; + if (!key.isEmpty()) + m_resources.remove(key); + + return; + } + + qDebug() << QString("Generated metadata for %1").arg(resource->name()); + emit metadataReady(resource); + + if (remove == RemoveFromList::Yes) + { + if (key.isEmpty()) + key = getExistingHash(resource); + m_resources.remove(key); + } +} + +void EnsureMetadataTask::emitFail(Resource* resource, QString key, RemoveFromList remove) +{ + if (!resource) + { + qCritical() << "Tried to mark a null resource as failed."; + if (!key.isEmpty()) + m_resources.remove(key); + + return; + } + + qDebug() << QString("Failed to generate metadata for %1").arg(resource->name()); + emit metadataFailed(resource); + + if (remove == RemoveFromList::Yes) + { + if (key.isEmpty()) + key = getExistingHash(resource); + m_resources.remove(key); + } +} + +// Modrinth + +Task::Ptr EnsureMetadataTask::modrinthVersionsTask() +{ + auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); + + auto response = std::make_shared(); + auto ver_task = modrinth_api.currentVersions(m_resources.keys(), hash_type, response); + + // Prevents unfortunate timings when aborting the task + if (!ver_task) + return Task::Ptr{ nullptr }; + + connect(ver_task.get(), + &Task::succeeded, + this, + [this, response] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + try + { + auto entries = Json::requireObject(doc); + for (auto& hash : m_resources.keys()) + { + auto resource = m_resources.find(hash).value(); + try + { + auto entry = Json::requireObject(entries, hash); + + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); + qDebug() << "Getting version for" << resource->name() << "from Modrinth"; + + m_tempVersions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(resource); + } + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return ver_task; +} + +Task::Ptr EnsureMetadataTask::modrinthProjectsTask() +{ + QHash addonIds; + for (auto const& data : m_tempVersions) + addonIds.insert(data.addonId.toString(), data.hash); + + auto response = std::make_shared(); + Task::Ptr proj_task; + + if (addonIds.isEmpty()) + { + qWarning() << "No addonId found!"; + } + else if (addonIds.size() == 1) + { + proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response); + } + else + { + proj_task = modrinth_api.getProjects(addonIds.keys(), response); + } + + // Prevents unfortunate timings when aborting the task + if (!proj_task) + return Task::Ptr{ nullptr }; + + connect(proj_task.get(), + &Task::succeeded, + this, + [this, response, addonIds] + { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + QJsonArray entries; + + try + { + if (addonIds.size() == 1) + entries = { doc.object() }; + else + entries = Json::requireArray(doc); + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + + for (auto entry : entries) + { + ModPlatform::IndexedPack pack; + + try + { + auto entry_obj = Json::requireObject(entry); + + Modrinth::loadIndexedPack(pack, entry_obj); + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + + // Skip this entry, since it has problems + continue; + } + + auto hashIt = addonIds.find(pack.addonId.toString()); + if (hashIt == addonIds.end()) + { + qWarning() << "Invalid project id from the API response."; + continue; + } + const auto& hash = hashIt.value(); + + auto resource_iter = m_resources.find(hash); + if (resource_iter == m_resources.end()) + { + qWarning() << "Invalid project id from the API response."; + continue; + } + + auto* resource = resource_iter.value(); + auto versionIter = m_tempVersions.find(hash); + if (versionIter == m_tempVersions.end()) + { + qWarning() << "Missing temporary version data for Modrinth project."; + continue; + } + + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); + + updateMetadata(pack, versionIter.value(), resource); + } + }); + + return proj_task; +} + +// Flame +Task::Ptr EnsureMetadataTask::flameVersionsTask() +{ + auto response = std::make_shared(); + + QList fingerprints; + for (auto& murmur : m_resources.keys()) + { + fingerprints.push_back(murmur.toUInt()); + } + + auto ver_task = flame_api.matchFingerprints(fingerprints, response); + + connect(ver_task.get(), + &Task::succeeded, + this, + [this, response] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + try + { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::requireObject(doc_obj, "data"); + auto data_arr = Json::requireArray(data_obj, "exactMatches"); + + if (data_arr.isEmpty()) + { + qWarning() << "No matches found for fingerprint search!"; + + return; + } + + for (auto match : data_arr) + { + auto match_obj = Json::ensureObject(match, {}); + auto file_obj = Json::ensureObject(match_obj, "file", {}); + + if (match_obj.isEmpty() || file_obj.isEmpty()) + { + qWarning() << "Fingerprint match is empty!"; + + return; + } + + auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt()); + auto resource = m_resources.find(fingerprint); + if (resource == m_resources.end()) + { + qWarning() << "Invalid fingerprint from the API response."; + continue; + } + + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*resource)->name())); + + m_tempVersions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return ver_task; +} + +Task::Ptr EnsureMetadataTask::flameProjectsTask() +{ + QHash addonIds; + for (auto const& hash : m_resources.keys()) + { + if (m_tempVersions.contains(hash)) + { + auto data = m_tempVersions.find(hash).value(); + + auto id_str = data.addonId.toString(); + if (!id_str.isEmpty()) + addonIds.insert(data.addonId.toString(), hash); + } + } + + auto response = std::make_shared(); + Task::Ptr proj_task; + + if (addonIds.isEmpty()) + { + qWarning() << "No addonId found!"; + } + else if (addonIds.size() == 1) + { + proj_task = flame_api.getProject(*addonIds.keyBegin(), response); + } + else + { + proj_task = flame_api.getProjects(addonIds.keys(), response); + } + + // Prevents unfortunate timings when aborting the task + if (!proj_task) + return Task::Ptr{ nullptr }; + + connect(proj_task.get(), + &Task::succeeded, + this, + [this, response, addonIds] + { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try + { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) + { + auto entry_obj = Json::requireObject(entry); + + auto id = QString::number(Json::requireInteger(entry_obj, "id")); + auto hashIt = addonIds.find(id); + if (hashIt == addonIds.end()) + { + qWarning() << "Invalid project id from the API response."; + continue; + } + const auto& hash = hashIt.value(); + auto resourceIt = m_resources.find(hash); + if (resourceIt == m_resources.end()) + { + qWarning() << "Invalid fingerprint from the API response."; + continue; + } + auto resource = resourceIt.value(); + auto versionIter = m_tempVersions.find(hash); + if (versionIter == m_tempVersions.end()) + { + qWarning() << "Missing temporary version data for CurseForge project."; + continue; + } + + ModPlatform::IndexedPack pack; + try + { + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); + + FlameMod::loadIndexedPack(pack, entry_obj); + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(resource); + } + updateMetadata(pack, versionIter.value(), resource); + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return proj_task; +} + +void EnsureMetadataTask::updateMetadata(ModPlatform::IndexedPack& pack, + ModPlatform::IndexedVersion& ver, + Resource* resource) +{ + try + { + // Prevent file name mismatch + ver.fileName = resource->fileinfo().fileName(); + if (ver.fileName.endsWith(".disabled")) + ver.fileName.chop(9); + + auto task = makeShared(m_indexDir, pack, ver); + + connect(task.get(), &Task::finished, this, [this, &pack, resource] { updateMetadataCallback(pack, resource); }); + + m_updateMetadataTasks[ModPlatform::ProviderCapabilities::name(pack.provider) + pack.addonId.toString()] = task; + task->start(); + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + + emitFail(resource); + } +} + +void EnsureMetadataTask::updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource) +{ + QDir tmpIndexDir(m_indexDir); + auto metadata = Metadata::get(tmpIndexDir, pack.slug); + if (!metadata.isValid()) + { + qCritical() << "Failed to generate metadata at last step!"; + emitFail(resource); + return; + } + + resource->setMetadata(metadata); + + emitReady(resource); +} diff --git a/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h new file mode 100644 index 0000000000..7578155783 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/EnsureMetadataTask.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "ModIndex.h" +#include "net/NetJob.h" + +#include "modplatform/helpers/HashUtils.h" + +#include "minecraft/mod/Resource.hpp" +#include "tasks/ConcurrentTask.h" + +class Mod; +class QDir; + +class EnsureMetadataTask : public Task +{ + Q_OBJECT + + public: + EnsureMetadataTask(Resource*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QList&, + QDir, + ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QHash&, + QDir, + ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + + ~EnsureMetadataTask() = default; + + Task::Ptr getHashingTask() + { + return m_hashingTask; + } + + public slots: + bool abort() override; + protected slots: + void executeTask() override; + + private: + // Platform-specific version/project fetching (kept together for consistency) + Task::Ptr modrinthVersionsTask(); + Task::Ptr modrinthProjectsTask(); + + Task::Ptr flameVersionsTask(); + Task::Ptr flameProjectsTask(); + + // Helpers + enum class RemoveFromList + { + Yes, + No + }; + void emitReady(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); + void emitFail(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); + + // Hashes and stuff + Hashing::Hasher::Ptr createNewHash(Resource*); + QString getExistingHash(Resource*); + + private slots: + void updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*); + void updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource); + + signals: + void metadataReady(Resource*); + void metadataFailed(Resource*); + + private: + QHash m_resources; + QDir m_indexDir; + ModPlatform::ResourceProvider m_provider; + + QHash m_tempVersions; + Task::Ptr m_hashingTask; + Task::Ptr m_currentTask; + QHash m_updateMetadataTasks; +}; diff --git a/archived/projt-launcher/launcher/modplatform/ModIndex.cpp b/archived/projt-launcher/launcher/modplatform/ModIndex.cpp new file mode 100644 index 0000000000..7499b81720 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/ModIndex.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "modplatform/ModIndex.h" + +#include +#include +#include + +namespace ModPlatform +{ + + static const QMap s_indexed_version_type_names = { + { "release", IndexedVersionType::VersionType::Release }, + { "beta", IndexedVersionType::VersionType::Beta }, + { "alpha", IndexedVersionType::VersionType::Alpha } + }; + + static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, + Fabric, Babric, BTA, LegacyFabric, Ornithe, + Rift, Risugami, StationLoader, ModLoaderMP, Optifine }; + + QList modLoaderTypesToList(ModLoaderTypes flags) + { + QList flagList; + for (auto flag : loaderList) + { + if (flags.testFlag(flag)) + { + flagList.append(flag); + } + } + return flagList; + } + + IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) + {} + + IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) + { + m_type = type; + } + + IndexedVersionType::IndexedVersionType(const IndexedVersionType& other) + { + m_type = other.m_type; + } + + IndexedVersionType& IndexedVersionType::operator=(const IndexedVersionType& other) + { + m_type = other.m_type; + return *this; + } + + const QString IndexedVersionType::toString(const IndexedVersionType::VersionType& type) + { + return s_indexed_version_type_names.key(type, "unknown"); + } + + IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString& type) + { + return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown); + } + + const char* ProviderCapabilities::name(ResourceProvider p) + { + switch (p) + { + case ResourceProvider::MODRINTH: return "modrinth"; + case ResourceProvider::FLAME: return "curseforge"; + } + return {}; + } + + QString ProviderCapabilities::readableName(ResourceProvider p) + { + switch (p) + { + case ResourceProvider::MODRINTH: return "Modrinth"; + case ResourceProvider::FLAME: return "CurseForge"; + } + return {}; + } + + QStringList ProviderCapabilities::hashType(ResourceProvider p) + { + switch (p) + { + case ResourceProvider::MODRINTH: return { "sha512", "sha1" }; + case ResourceProvider::FLAME: + // Try newer formats first, fall back to old format + return { "sha1", "md5", "murmur2" }; + } + return {}; + } + + QString getMetaURL(ResourceProvider provider, QVariant projectID) + { + return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" + : "https://modrinth.com/mod/") + + projectID.toString(); + } + + auto getModLoaderAsString(ModLoaderType type) -> const QString + { + switch (type) + { + case NeoForge: return "neoforge"; + case Forge: return "forge"; + case Cauldron: return "cauldron"; + case LiteLoader: return "liteloader"; + case Fabric: return "fabric"; + case Quilt: return "quilt"; + case DataPack: return "datapack"; + case Babric: return "babric"; + case BTA: return "bta-babric"; + case LegacyFabric: return "legacy-fabric"; + case Ornithe: return "ornithe"; + case Rift: return "rift"; + case Risugami: return "risugami"; + case StationLoader: return "station-loader"; + case ModLoaderMP: return "modloadermp"; + case Optifine: return "optifine"; + default: break; + } + return ""; + } + + auto getModLoaderFromString(QString type) -> ModLoaderType + { + if (type == "neoforge") + return NeoForge; + if (type == "forge") + return Forge; + if (type == "cauldron") + return Cauldron; + if (type == "liteloader") + return LiteLoader; + if (type == "fabric") + return Fabric; + if (type == "quilt") + return Quilt; + if (type == "babric") + return Babric; + if (type == "bta-babric") + return BTA; + if (type == "legacy-fabric") + return LegacyFabric; + if (type == "ornithe") + return Ornithe; + if (type == "rift") + return Rift; + if (type == "risugami") + return Risugami; + if (type == "station-loader") + return StationLoader; + if (type == "modloadermp") + return ModLoaderMP; + if (type == "optifine") + return Optifine; + return {}; + } + + QString SideUtils::toString(Side side) + { + switch (side) + { + case Side::ClientSide: return "client"; + case Side::ServerSide: return "server"; + case Side::UniversalSide: return "both"; + case Side::NoSide: break; + } + return {}; + } + + Side SideUtils::fromString(QString side) + { + if (side == "client") + return Side::ClientSide; + if (side == "server") + return Side::ServerSide; + if (side == "both") + return Side::UniversalSide; + return Side::UniversalSide; + } +} // namespace ModPlatform diff --git a/archived/projt-launcher/launcher/modplatform/ModIndex.h b/archived/projt-launcher/launcher/modplatform/ModIndex.h new file mode 100644 index 0000000000..c168fef72d --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/ModIndex.h @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include + +class QIODevice; + +namespace ModPlatform +{ + + enum ModLoaderType + { + NeoForge = 1 << 0, + Forge = 1 << 1, + Cauldron = 1 << 2, + LiteLoader = 1 << 3, + Fabric = 1 << 4, + Quilt = 1 << 5, + DataPack = 1 << 6, + Babric = 1 << 7, + BTA = 1 << 8, + LegacyFabric = 1 << 9, + Ornithe = 1 << 10, + Rift = 1 << 11, + Risugami = 1 << 12, + StationLoader = 1 << 13, + ModLoaderMP = 1 << 14, + Optifine = 1 << 15 + }; + Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) + QList modLoaderTypesToList(ModLoaderTypes flags); + + enum class ResourceProvider + { + MODRINTH, + FLAME + }; + + enum class DependencyType + { + REQUIRED, + OPTIONAL, + INCOMPATIBLE, + EMBEDDED, + TOOL, + INCLUDE, + UNKNOWN + }; + + enum class Side + { + NoSide = 0, + ClientSide = 1 << 0, + ServerSide = 1 << 1, + UniversalSide = ClientSide | ServerSide + }; + + namespace SideUtils + { + QString toString(Side side); + Side fromString(QString side); + } // namespace SideUtils + + namespace ProviderCapabilities + { + const char* name(ResourceProvider); + QString readableName(ResourceProvider); + QStringList hashType(ResourceProvider); + } // namespace ProviderCapabilities + + struct ModpackAuthor + { + QString name; + QString url; + }; + + struct DonationData + { + QString id; + QString platform; + QString url; + }; + + struct IndexedVersionType + { + enum class VersionType + { + Release = 1, + Beta, + Alpha, + Unknown + }; + IndexedVersionType(const QString& type); + IndexedVersionType(const IndexedVersionType::VersionType& type); + IndexedVersionType(const IndexedVersionType& type); + IndexedVersionType() : IndexedVersionType(IndexedVersionType::VersionType::Unknown) + {} + static const QString toString(const IndexedVersionType::VersionType& type); + static IndexedVersionType::VersionType enumFromString(const QString& type); + bool isValid() const + { + return m_type != IndexedVersionType::VersionType::Unknown; + } + IndexedVersionType& operator=(const IndexedVersionType& other); + bool operator==(const IndexedVersionType& other) const + { + return m_type == other.m_type; + } + bool operator==(const IndexedVersionType::VersionType& type) const + { + return m_type == type; + } + bool operator!=(const IndexedVersionType& other) const + { + return m_type != other.m_type; + } + bool operator!=(const IndexedVersionType::VersionType& type) const + { + return m_type != type; + } + bool operator<(const IndexedVersionType& other) const + { + return m_type < other.m_type; + } + bool operator<(const IndexedVersionType::VersionType& type) const + { + return m_type < type; + } + bool operator<=(const IndexedVersionType& other) const + { + return m_type <= other.m_type; + } + bool operator<=(const IndexedVersionType::VersionType& type) const + { + return m_type <= type; + } + bool operator>(const IndexedVersionType& other) const + { + return m_type > other.m_type; + } + bool operator>(const IndexedVersionType::VersionType& type) const + { + return m_type > type; + } + bool operator>=(const IndexedVersionType& other) const + { + return m_type >= other.m_type; + } + bool operator>=(const IndexedVersionType::VersionType& type) const + { + return m_type >= type; + } + + QString toString() const + { + return toString(m_type); + } + + IndexedVersionType::VersionType m_type; + }; + + struct Dependency + { + QVariant addonId; + DependencyType type; + QString version; + }; + + struct IndexedVersion + { + QVariant addonId; + QVariant fileId; + QString version; + QString version_number = {}; + IndexedVersionType version_type; + QStringList mcVersion; + QString downloadUrl; + QString date; + QString fileName; + ModLoaderTypes loaders = {}; + QString hash_type; + QString hash; + bool is_preferred = true; + QString changelog; + QList dependencies; + Side side; // this is for flame API + QString relativePath; // Generic target path override + + // For internal use, not provided by APIs + bool is_currently_selected = false; + + QString getVersionDisplayString() const + { + auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : ""; + auto versionStr = !version.contains(version_number) ? version_number : ""; + QString gameVersion = ""; + for (auto v : mcVersion) + { + if (version.contains(v)) + { + gameVersion = ""; + break; + } + if (gameVersion.isEmpty()) + { + gameVersion = QObject::tr(" for %1").arg(v); + } + } + return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type); + } + }; + + struct ExtraPackData + { + QList donate; + + QString issuesUrl; + QString sourceUrl; + QString wikiUrl; + QString discordUrl; + + QString status; + + QString body; + }; + + struct IndexedPack + { + using Ptr = std::shared_ptr; + + QVariant addonId; + ResourceProvider provider; + QString name; + QString slug; + QString description; + QList authors; + QString logoName; + QString logoUrl; + QString websiteUrl; + Side side; + + bool versionsLoaded = false; + QList versions; + + // Don't load by default, since some modplatform don't have that info + bool extraDataLoaded = true; + ExtraPackData extraData; + + // For internal use, not provided by APIs + bool isVersionSelected(int index) const + { + if (!versionsLoaded) + return false; + + return versions.at(index).is_currently_selected; + } + bool isAnyVersionSelected() const + { + if (!versionsLoaded) + return false; + + return std::any_of(versions.constBegin(), + versions.constEnd(), + [](auto const& v) { return v.is_currently_selected; }); + } + }; + + struct OverrideDep + { + QString quilt; + QString fabric; + QString slug; + ModPlatform::ResourceProvider provider; + }; + + inline auto getOverrideDeps() -> QList + { + return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME }, + { "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME }, + + { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, + { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; + } + + QString getMetaURL(ResourceProvider provider, QVariant projectID); + + auto getModLoaderAsString(ModLoaderType type) -> const QString; + auto getModLoaderFromString(QString type) -> ModLoaderType; + + constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept + { + auto x = static_cast(l); + return x && !(x & (x - 1)); + } + + struct Category + { + QString name; + QString id; + }; + +} // namespace ModPlatform + +Q_DECLARE_METATYPE(ModPlatform::IndexedPack) +Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr) +Q_DECLARE_METATYPE(ModPlatform::ResourceProvider) diff --git a/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp b/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp new file mode 100644 index 0000000000..324f2d41d9 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "modplatform/ResourceAPI.h" + +#include "Application.h" +#include "Json.h" +#include "net/NetJob.h" + +#include "modplatform/ModIndex.h" + +#include "net/ApiDownload.h" + +Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, + Callback>&& callbacks) const +{ + auto search_url_optional = getSearchURL(args); + if (!search_url_optional.has_value()) + { + callbacks.on_fail("Failed to create search URL", -1); + return nullptr; + } + + auto search_url = search_url_optional.value(); + + auto response = std::make_shared(); + auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response)); + + QObject::connect(netJob.get(), + &NetJob::succeeded, + [this, response, callbacks] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + + callbacks.on_fail(parse_error.errorString(), -1); + + return; + } + + QList newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) + { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack::Ptr pack = std::make_shared(); + try + { + loadIndexedPack(*pack, packObj); + newList << pack; + } + catch (const JSONValidationError& e) + { + qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause(); + continue; + } + } + + callbacks.on_succeed(newList); + }); + + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), + &NetJob::failed, + [weak, callbacks](const QString& reason) + { + int network_error_code = -1; + if (auto netJob = weak.lock()) + { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(netJob.get(), + &NetJob::aborted, + [callbacks] + { + if (callbacks.on_abort != nullptr) + callbacks.on_abort(); + }); + + return netJob; +} + +Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, + Callback>&& callbacks) const +{ + auto versions_url_optional = getVersionsURL(args); + if (!versions_url_optional.has_value()) + return nullptr; + + auto versions_url = versions_url_optional.value(); + + auto netJob = makeShared(QString("%1::Versions").arg(args.pack->name), APPLICATION->network()); + auto response = std::make_shared(); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); + + QObject::connect(netJob.get(), + &NetJob::succeeded, + [this, response, callbacks, args] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response for getting versions at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + QVector unsortedVersions; + try + { + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + + for (auto versionIter : arr) + { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj, args.resourceType); + if (!file.addonId.isValid()) + file.addonId = args.pack->addonId; + + if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the + // returned value is valid + unsortedVersions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, + const ModPlatform::IndexedVersion& b) -> bool + { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + } + catch (const JSONValidationError& e) + { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause(); + } + + callbacks.on_succeed(unsortedVersions); + }); + + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), + &NetJob::failed, + [weak, callbacks](const QString& reason) + { + int network_error_code = -1; + if (auto netJob = weak.lock()) + { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(netJob.get(), + &NetJob::aborted, + [callbacks] + { + if (callbacks.on_abort != nullptr) + callbacks.on_abort(); + }); + + return netJob; +} + +Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback&& callbacks) const +{ + auto response = std::make_shared(); + auto job = getProject(args.pack->addonId.toString(), response); + + QObject::connect(job.get(), + &NetJob::succeeded, + [this, response, callbacks, args] + { + auto pack = args.pack; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + try + { + auto obj = Json::requireObject(doc); + if (obj.contains("data")) + obj = Json::requireObject(obj, "data"); + loadIndexedPack(*pack, obj); + loadExtraPackInfo(*pack, obj); + } + catch (const JSONValidationError& e) + { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); + } + callbacks.on_succeed(pack); + }); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = job.toWeakRef(); + QObject::connect(job.get(), + &NetJob::failed, + [weak, callbacks](const QString& reason) + { + int network_error_code = -1; + if (auto job = weak.lock()) + { + if (auto netJob = qSharedPointerDynamicCast(job)) + { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + { + network_error_code = failed_action->replyStatusCode(); + } + } + } + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(job.get(), + &NetJob::aborted, + [callbacks] + { + if (callbacks.on_abort != nullptr) + callbacks.on_abort(); + }); + return job; +} + +Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, + Callback&& callbacks) const +{ + auto versions_url_optional = getDependencyURL(args); + if (!versions_url_optional.has_value()) + return nullptr; + + auto versions_url = versions_url_optional.value(); + + auto netJob = + makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); + auto response = std::make_shared(); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); + + QObject::connect(netJob.get(), + &NetJob::succeeded, + [this, response, callbacks, args] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response for getting versions at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + QJsonArray arr; + if (args.dependency.version.length() != 0 && doc.isObject()) + { + arr.append(doc.object()); + } + else + { + arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + } + + QVector versions; + for (auto versionIter : arr) + { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod); + if (!file.addonId.isValid()) + file.addonId = args.dependency.addonId; + + if (file.fileId.isValid() && (!file.loaders || args.loader & file.loaders)) // Heuristic to + // check if the + // returned + // value is + // valid + versions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, + const ModPlatform::IndexedVersion& b) -> bool + { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(versions.begin(), versions.end(), orderSortPredicate); + auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion(); + callbacks.on_succeed(bestMatch); + }); + + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), + &NetJob::failed, + [weak, callbacks](const QString& reason) + { + int network_error_code = -1; + if (auto netJob = weak.lock()) + { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); + return netJob; +} + +QString ResourceAPI::getGameVersionsString(std::list mcVersions) const +{ + QString s; + for (auto& ver : mcVersions) + { + s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); + } + s.remove(s.length() - 1, 1); // remove last comma + return s; +} + +QString ResourceAPI::mapMCVersionToModrinth(Version v) const +{ + static const QString preString = " Pre-Release "; + auto verStr = v.toString(); + + if (verStr.contains(preString)) + { + verStr.replace(preString, "-pre"); + } + verStr.replace(" ", "-"); + return verStr; +} + +Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr response) const +{ + auto project_url_optional = getInfoURL(addonId); + if (!project_url_optional.has_value()) + return nullptr; + + auto project_url = project_url_optional.value(); + + auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); + + return netJob; +} diff --git a/archived/projt-launcher/launcher/modplatform/ResourceAPI.h b/archived/projt-launcher/launcher/modplatform/ResourceAPI.h new file mode 100644 index 0000000000..3762ba22de --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/ResourceAPI.h @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023-2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "../Version.h" + +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceType.h" +#include "tasks/Task.h" + +/* Simple class with a common interface for interacting with APIs */ +class ResourceAPI +{ + public: + virtual ~ResourceAPI() = default; + + struct SortingMethod + { + // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods. + // Used by Flame in the API request. + unsigned int index; + // The real name of the sorting, as used in the respective API specification. + // Used by Modrinth in the API request. + QString name; + // The human-readable name of the sorting, used for display in the UI. + QString readable_name; + }; + + template + struct Callback + { + std::function on_succeed; + std::function on_fail; + std::function on_abort; + }; + + struct SearchArgs + { + ModPlatform::ResourceType type{}; + int offset = 0; + + std::optional search; + std::optional sorting; + std::optional loaders; + std::optional> versions; + std::optional side; + std::optional categoryIds; + bool openSource; + }; + + struct VersionSearchArgs + { + ModPlatform::IndexedPack::Ptr pack; + + std::optional> mcVersions; + std::optional loaders; + ModPlatform::ResourceType resourceType; + }; + + struct ProjectInfoArgs + { + ModPlatform::IndexedPack::Ptr pack; + }; + + struct DependencySearchArgs + { + ModPlatform::Dependency dependency; + Version mcVersion; + ModPlatform::ModLoaderTypes loader; + }; + + public: + /** Gets a list of available sorting methods for this API. */ + virtual auto getSortingMethods() const -> QList = 0; + + public slots: + virtual Task::Ptr searchProjects(SearchArgs&&, Callback>&&) const; + + virtual Task::Ptr getProject(QString addonId, std::shared_ptr response) const; + virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const = 0; + + virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback&&) const; + Task::Ptr getProjectVersions(VersionSearchArgs&& args, + Callback>&& callbacks) const; + virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback&&) const; + + protected: + inline QString debugName() const + { + return "External resource API"; + } + + QString mapMCVersionToModrinth(Version v) const; + + QString getGameVersionsString(std::list mcVersions) const; + + public: + virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; + virtual auto getInfoURL(QString const& id) const -> std::optional = 0; + virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; + virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0; + virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0; + + /** Converts a JSON document to a common array format. + * + * This is needed so that different providers, with different JSON structures, can be parsed + * uniformally. You NEED to re-implement this if you intend on using the default callbacks. + */ + virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0; +}; diff --git a/archived/projt-launcher/launcher/modplatform/ResourceType.cpp b/archived/projt-launcher/launcher/modplatform/ResourceType.cpp new file mode 100644 index 0000000000..badbab541c --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/ResourceType.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * ======================================================================== */ + +#include "ResourceType.h" + +namespace ModPlatform +{ + static const QMap s_packedTypeNames = { + { ResourceType::ResourcePack, QObject::tr("resource pack") }, + { ResourceType::TexturePack, QObject::tr("texture pack") }, + { ResourceType::DataPack, QObject::tr("data pack") }, + { ResourceType::ShaderPack, QObject::tr("shader pack") }, + { ResourceType::World, QObject::tr("world save") }, + { ResourceType::Mod, QObject::tr("mod") }, + { ResourceType::Unknown, QObject::tr("unknown") } + }; + + namespace ResourceTypeUtils + { + + QString getName(ResourceType type) + { + const auto typeIt = s_packedTypeNames.constFind(type); + if (typeIt != s_packedTypeNames.cend()) + return *typeIt; + return s_packedTypeNames.value(ResourceType::Unknown); + } + + } // namespace ResourceTypeUtils +} // namespace ModPlatform diff --git a/archived/projt-launcher/launcher/modplatform/ResourceType.h b/archived/projt-launcher/launcher/modplatform/ResourceType.h new file mode 100644 index 0000000000..c2ec5ddaf3 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/ResourceType.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * + * + * ======================================================================== */ + +#pragma once + +#include + +#include +#include +#include + +namespace ModPlatform +{ + + enum class ResourceType + { + Mod, + ResourcePack, + ShaderPack, + Modpack, + DataPack, + World, + Screenshots, + TexturePack, + Unknown + }; + + namespace ResourceTypeUtils + { + static const std::set VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, + ResourceType::TexturePack, ResourceType::ShaderPack, + ResourceType::World, ResourceType::Mod }; + QString getName(ResourceType type); + } // namespace ResourceTypeUtils +} // namespace ModPlatform \ No newline at end of file diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp new file mode 100644 index 0000000000..8e2be2cd75 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#include "ATLPackIndex.h" + +#include + +#include "Json.h" + +static void loadIndexedVersion(ATLauncher::IndexedVersion& v, QJsonObject& obj) +{ + v.version = Json::requireString(obj, "version"); + v.minecraft = Json::requireString(obj, "minecraft"); +} + +void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.position = Json::requireInteger(obj, "position"); + m.name = Json::requireString(obj, "name"); + m.type = + Json::requireString(obj, "type") == "private" ? ATLauncher::PackType::Private : ATLauncher::PackType::Public; + auto versionsArr = Json::requireArray(obj, "versions"); + for (const auto versionRaw : versionsArr) + { + auto versionObj = Json::requireObject(versionRaw); + ATLauncher::IndexedVersion version; + loadIndexedVersion(version, versionObj); + m.versions.append(version); + } + m.system = Json::ensureBoolean(obj, QString("system"), false); + m.description = Json::ensureString(obj, "description", ""); + + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png"; +} diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h new file mode 100644 index 0000000000..947b680763 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include "ATLPackManifest.h" + +#include +#include +#include + +namespace ATLauncher +{ + + struct IndexedVersion + { + QString version; + QString minecraft; + }; + + struct IndexedPack + { + int id; + int position; + QString name; + PackType type; + QList versions; + bool system; + QString description; + + QString safeName; + }; + + void loadIndexedPack(IndexedPack& m, QJsonObject& obj); +} // namespace ATLauncher + +Q_DECLARE_METATYPE(ATLauncher::IndexedPack) +Q_DECLARE_METATYPE(QList) \ No newline at end of file diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp new file mode 100644 index 0000000000..a870d387dc --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -0,0 +1,1273 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * ======================================================================== */ + +#include "ATLPackInstallTask.h" + +#include +#include + +#include + +#include "FileSystem.h" +#include "Json.h" +#include "MMCZip.h" +#include "Version.h" +#include "meta/Index.hpp" +#include "meta/Version.hpp" +#include "meta/VersionList.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/PackProfile.h" +#include "modplatform/atlauncher/ATLPackManifest.h" +#include "net/ChecksumValidator.h" +#include "settings/INISettingsObject.h" + +#include "net/ApiDownload.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "ui/dialogs/BlockedModsDialog.h" + +namespace ATLauncher +{ + + static projt::meta::MetaVersion::Ptr getComponentVersion(const QString& uid, const QString& version); + + PackInstallTask::PackInstallTask(UserInteractionSupport* support, + QString packName, + QString version, + InstallMode installMode) + { + m_support = support; + m_pack_name = packName; + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m_pack_safe_name = packName.replace(s_regex, ""); + m_version_name = version; + m_install_mode = installMode; + } + + bool PackInstallTask::abort() + { + if (abortable) + { + return jobPtr->abort(); + } + return false; + } + + void PackInstallTask::executeTask() + { + qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); + NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; + auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") + .arg(m_pack_safe_name) + .arg(m_version_name); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + + connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); + + jobPtr = netJob; + jobPtr->start(); + } + + void PackInstallTask::onDownloadSucceeded() + { + qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); + jobPtr.reset(); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from ATLauncher at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response.get(); + return; + } + auto obj = doc.object(); + + ATLauncher::PackVersion version; + try + { + ATLauncher::loadVersion(version, obj); + } + catch (const JSONValidationError& e) + { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + return; + } + version.pack = m_pack_safe_name; + m_version = version; + + // Derived from the installation mode + QString message; + bool resetDirectory; + + switch (m_install_mode) + { + case InstallMode::Reinstall: + case InstallMode::Update: + message = m_version.messages.update; + resetDirectory = true; + break; + + case InstallMode::Install: + message = m_version.messages.install; + resetDirectory = false; + break; + + default: emitFailed(tr("Unsupported installation mode")); return; + } + + // Display message if one exists + if (!message.isEmpty()) + m_support->displayMessage(message); + + auto ver = getComponentVersion("net.minecraft", m_version.minecraft); + if (!ver) + { + emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); + return; + } + minecraftVersion = ver; + + if (resetDirectory) + { + deleteExistingFiles(); + } + + if (m_version.noConfigs) + { + downloadMods(); + } + else + { + installConfigs(); + } + } + + void PackInstallTask::onDownloadFailed(QString reason) + { + qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId(); + jobPtr.reset(); + emitFailed(reason); + } + + void PackInstallTask::onDownloadAborted() + { + jobPtr.reset(); + emitAborted(); + } + + void PackInstallTask::deleteExistingFiles() + { + setStatus(tr("Deleting existing files...")); + + // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/delete + VersionDeletes deletes; + deletes.folders.append(VersionDelete{ "root", "mods%s%" }); + deletes.folders.append(VersionDelete{ "root", "configs%s%" }); + deletes.folders.append(VersionDelete{ "root", "bin%s%" }); + + // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/keep + VersionKeeps keeps; + keeps.files.append(VersionKeep{ "root", "mods%s%PortalGunSounds.pak" }); + keeps.folders.append(VersionKeep{ "root", "mods%s%rei_minimap%s%" }); + keeps.folders.append(VersionKeep{ "root", "mods%s%VoxelMods%s%" }); + keeps.files.append(VersionKeep{ "root", "config%s%NEI.cfg" }); + keeps.files.append(VersionKeep{ "root", "options.txt" }); + keeps.files.append(VersionKeep{ "root", "servers.dat" }); + + // Merge with version deletes and keeps + for (const auto& item : m_version.deletes.files) + deletes.files.append(item); + for (const auto& item : m_version.deletes.folders) + deletes.folders.append(item); + for (const auto& item : m_version.keeps.files) + keeps.files.append(item); + for (const auto& item : m_version.keeps.folders) + keeps.folders.append(item); + + auto getPathForBase = [this](const QString& base) + { + auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft"); + + if (base == "root") + { + return minecraftPath; + } + else if (base == "config") + { + return FS::PathCombine(minecraftPath, "config"); + } + else + { + qWarning() << "Unrecognised base path" << base; + return minecraftPath; + } + }; + + auto convertToSystemPath = [](const QString& path) + { + auto t = path; + t.replace("%s%", QDir::separator()); + return t; + }; + + auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) + { + for (const auto& item : keeps.files) + { + auto basePath = getPathForBase(item.base); + auto targetPath = convertToSystemPath(item.target); + auto path = FS::PathCombine(basePath, targetPath); + + if (fullPath == path) + { + return true; + } + } + + for (const auto& item : keeps.folders) + { + auto basePath = getPathForBase(item.base); + auto targetPath = convertToSystemPath(item.target); + auto path = FS::PathCombine(basePath, targetPath); + + if (fullPath.startsWith(path)) + { + return true; + } + } + + return false; + }; + + // Keep track of files to delete + QSet filesToDelete; + + for (const auto& item : deletes.files) + { + auto basePath = getPathForBase(item.base); + auto targetPath = convertToSystemPath(item.target); + auto fullPath = FS::PathCombine(basePath, targetPath); + + if (shouldKeep(fullPath)) + continue; + + filesToDelete.insert(fullPath); + } + + for (const auto& item : deletes.folders) + { + auto basePath = getPathForBase(item.base); + auto targetPath = convertToSystemPath(item.target); + auto fullPath = FS::PathCombine(basePath, targetPath); + + QDirIterator it(fullPath, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto path = it.next(); + + if (shouldKeep(path)) + continue; + + filesToDelete.insert(path); + } + } + + // Delete the files + for (const auto& item : filesToDelete) + { + FS::deletePath(item); + } + } + + QString PackInstallTask::getDirForModType(ModType type, QString raw) + { + switch (type) + { + // Mod types that can either be ignored at this stage, or ignored + // completely. + case ModType::Root: + case ModType::Extract: + case ModType::Decomp: + case ModType::TexturePackExtract: + case ModType::ResourcePackExtract: + case ModType::MCPC: return Q_NULLPTR; + case ModType::Forge: + // Forge detection happens later on, if it cannot be detected it will + // install a jarmod component. + case ModType::Jar: return "jarmods"; + case ModType::Mods: return "mods"; + case ModType::Flan: return "Flan"; + case ModType::Dependency: return FS::PathCombine("mods", m_version.minecraft); + case ModType::Ic2Lib: return FS::PathCombine("mods", "ic2"); + case ModType::DenLib: return FS::PathCombine("mods", "denlib"); + case ModType::Coremods: return "coremods"; + case ModType::Plugins: return "plugins"; + case ModType::TexturePack: return "texturepacks"; + case ModType::ResourcePack: return "resourcepacks"; + case ModType::ShaderPack: return "shaderpacks"; + case ModType::Millenaire: qWarning() << "Unsupported mod type: " + raw; return Q_NULLPTR; + case ModType::Unknown: emitFailed(tr("Unknown mod type: %1").arg(raw)); return Q_NULLPTR; + } + + return Q_NULLPTR; + } + + QString PackInstallTask::getVersionForLoader(QString uid) + { + if (m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) + { + auto vlist = APPLICATION->metadataIndex()->component(uid); + if (!vlist) + { + emitFailed(tr("Failed to get local metadata index for %1").arg(uid)); + return Q_NULLPTR; + } + + vlist->waitUntilReady(); + + if (m_version.loader.recommended || m_version.loader.latest) + { + for (int i = 0; i < vlist->allVersions().size(); i++) + { + auto version = vlist->allVersions().at(i); + auto reqs = version->dependencies(); + + // filter by minecraft version, if the loader depends on a certain version. + // not all mod loaders depend on a given Minecraft version, so we won't do this + // filtering for those loaders. + if (m_version.loader.type != "fabric") + { + auto iter = std::find_if(reqs.begin(), + reqs.end(), + [](const projt::meta::ComponentDependency& req) + { return req.uid == "net.minecraft"; }); + if (iter == reqs.end()) + continue; + if (iter->equalsVersion != m_version.minecraft) + continue; + } + + if (m_version.loader.recommended) + { + // first recommended build we find, we use. + if (!version->isStable()) + continue; + } + + return version->descriptor(); + } + + emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); + return Q_NULLPTR; + } + else if (m_version.loader.choose) + { + // Fabric Loader doesn't depend on a given Minecraft version. + if (m_version.loader.type == "fabric") + { + return m_support->chooseVersion(vlist, Q_NULLPTR); + } + + return m_support->chooseVersion(vlist, m_version.minecraft); + } + } + + if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) + { + emitFailed(tr("No loader version set for modpack!")); + return Q_NULLPTR; + } + + return m_version.loader.version; + } + + QString PackInstallTask::detectLibrary(const VersionLibrary& library) + { + // Try to detect what the library is + if (!library.server.isEmpty() && library.server.split("/").length() >= 3) + { + auto lastSlash = library.server.lastIndexOf("/"); + auto locationAndVersion = library.server.mid(0, lastSlash); + auto fileName = library.server.mid(lastSlash + 1); + + lastSlash = locationAndVersion.lastIndexOf("/"); + auto location = locationAndVersion.mid(0, lastSlash); + auto version = locationAndVersion.mid(lastSlash + 1); + + lastSlash = location.lastIndexOf("/"); + auto group = location.mid(0, lastSlash).replace("/", "."); + auto artefact = location.mid(lastSlash + 1); + + return group + ":" + artefact + ":" + version; + } + + if (library.file.contains("-")) + { + auto lastSlash = library.file.lastIndexOf("-"); + auto name = library.file.mid(0, lastSlash); + auto version = library.file.mid(lastSlash + 1).remove(".jar"); + + if (name == QString("guava")) + { + return "com.google.guava:guava:" + version; + } + else if (name == QString("commons-lang3")) + { + return "org.apache.commons:commons-lang3:" + version; + } + } + + return "org.multimc.atlauncher:" + library.md5 + ":1"; + } + + bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr profile) + { + if (m_version.libraries.isEmpty()) + { + return true; + } + + QList exempt; + for (const auto& componentUid : componentsToInstall.keys()) + { + auto componentIt = componentsToInstall.constFind(componentUid); + if (componentIt == componentsToInstall.cend() || !componentIt.value()) + continue; + const auto& componentVersion = componentIt.value(); + const auto& detailedData = componentVersion->detailedData(); + if (detailedData) + { + for (const auto& library : detailedData->libraries) + { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } + } + } + + const auto& minecraftDetailedData = minecraftVersion->detailedData(); + if (minecraftDetailedData) + { + for (const auto& library : minecraftDetailedData->libraries) + { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } + } + + auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); + auto target_id = "org.multimc.atlauncher." + id; + + auto patchDir = FS::PathCombine(instanceRoot, "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + auto f = std::make_shared(); + f->name = m_pack_name + " " + m_version_name + " (libraries)"; + + const static QMap liteLoaderMap = { + { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, + { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, + { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, + { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, + { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, + { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, + { "55785ccc82c07ff0ba038fe24be63ea2", "1.7.10_01" }, + { "63ada46e033d0cb6782bada09ad5ca4e", "1.7.10_04" }, + { "7983e4b28217c9ae8569074388409c86", "1.7.10_03" }, + { "c09882458d74fe0697c7681b8993097e", "1.7.10_02" }, + { "db7235aefd407ac1fde09a7baba50839", "1.7.10_00" }, + { "6e9028816027f53957bd8fcdfabae064", "1.8" }, + { "5e732dc446f9fe2abe5f9decaec40cde", "1.10-SNAPSHOT" }, + { "3a98b5ed95810bf164e71c1a53be568d", "1.11.2-SNAPSHOT" }, + { "ba8e6285966d7d988a96496f48cbddaa", "1.8.9-SNAPSHOT" }, + { "8524af3ac3325a82444cc75ae6e9112f", "1.11-SNAPSHOT" }, + { "53639d52340479ccf206a04f5e16606f", "1.5.2_01" }, + { "1fcdcf66ce0a0806b7ad8686afdce3f7", "1.6.4_00" }, + { "531c116f71ae2b11033f9a11a0f8e668", "1.6.4_01" }, + { "4009eeb99c9068f608d3483a6439af88", "1.7.2_03" }, + { "66f343354b8417abce1a10d557d2c6e9", "1.7.2_04" }, + { "ab554c21f28fbc4ae9b098bcb5f4cceb", "1.7.2_05" }, + { "e1d76a05a3723920e2f80a5e66c45f16", "1.7.2_02" }, + { "00318cb0c787934d523f63cdfe8ddde4", "1.9-SNAPSHOT" }, + { "986fd1ee9525cb0dcab7609401cef754", "1.9.4-SNAPSHOT" }, + { "571ad5e6edd5ff40259570c9be588bb5", "1.9.4" }, + { "1cdd72f7232e45551f16cc8ffd27ccf3", "1.10.2-SNAPSHOT" }, + { "8a7c21f32d77ee08b393dd3921ced8eb", "1.10.2" }, + { "b9bef8abc8dc309069aeba6fbbe58980", "1.12.1-SNAPSHOT" } + }; + + for (const auto& lib : m_version.libraries) + { + // If the library is LiteLoader, we need to ignore it and handle it separately. + if (liteLoaderMap.contains(lib.md5)) + { + auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5)); + if (ver) + { + componentsToInstall.insert("com.mumfrey.liteloader", ver); + continue; + } + } + + auto libName = detectLibrary(lib); + GradleSpecifier libSpecifier(libName); + + bool libExempt = false; + for (const auto& existingLib : exempt) + { + if (libSpecifier.matchName(existingLib)) + { + // If the pack specifies a newer version of the lib, use that! + libExempt = Version(libSpecifier.version()) >= Version(existingLib.version()); + } + } + if (libExempt) + continue; + + auto library = std::make_shared(); + library->setRawName(libName); + + switch (lib.download) + { + case DownloadType::Server: + library->setAbsoluteUrl(BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url); + break; + case DownloadType::Direct: library->setAbsoluteUrl(lib.url); break; + case DownloadType::Browser: + case DownloadType::Unknown: + emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw)); + return false; + } + + f->libraries.append(library); + } + + if (f->libraries.isEmpty()) + { + return true; + } + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); + return true; + } + + bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr profile) + { + if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) + { + return true; + } + + auto mainClass = m_version.mainClass.mainClass; + auto extraArguments = m_version.extraArguments.arguments; + + auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty(); + auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty(); + if (hasMainClassDepends || hasExtraArgumentsDepends) + { + QSet mods; + for (const auto& item : m_version.mods) + { + mods.insert(item.name); + } + + if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) + { + mainClass = ""; + } + + if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) + { + extraArguments = ""; + } + } + + if (mainClass.isEmpty() && extraArguments.isEmpty()) + { + return true; + } + + auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); + auto target_id = "org.multimc.atlauncher." + id; + + auto patchDir = FS::PathCombine(instanceRoot, "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QStringList mainClasses; + QStringList tweakers; + for (const auto& componentUid : componentsToInstall.keys()) + { + auto componentIt = componentsToInstall.constFind(componentUid); + if (componentIt == componentsToInstall.cend() || !componentIt.value()) + continue; + const auto& componentVersion = componentIt.value(); + const auto& detailedData = componentVersion->detailedData(); + if (detailedData) + { + if (detailedData->mainClass != QString("")) + { + mainClasses.append(detailedData->mainClass); + } + tweakers.append(detailedData->addTweakers); + } + } + + auto f = std::make_shared(); + f->name = m_pack_name + " " + m_version_name; + if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) + { + f->mainClass = mainClass; + } + + // Parse out tweakers + auto args = extraArguments.split(" "); + QString previous; + for (auto arg : args) + { + if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") + { + auto tweakClass = arg.remove("--tweakClass="); + if (tweakers.contains(tweakClass)) + continue; + + f->addTweakers.append(tweakClass); + } + previous = arg; + } + + if (f->mainClass == QString() && f->addTweakers.isEmpty()) + { + return true; + } + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); + return true; + } + + void PackInstallTask::installConfigs() + { + qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId(); + setStatus(tr("Downloading configs...")); + jobPtr.reset(new NetJob(tr("Config download"), APPLICATION->network())); + + auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") + .arg(m_pack_safe_name) + .arg(m_version_name); + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); + entry->setStale(true); + + auto dl = Net::ApiDownload::makeCached(url, entry); + if (!m_version.configs.sha1.isEmpty()) + { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, m_version.configs.sha1)); + } + jobPtr->addNetAction(dl); + archivePath = entry->getFullPath(); + + connect(jobPtr.get(), + &NetJob::succeeded, + this, + [this]() + { + abortable = false; + jobPtr.reset(); + extractConfigs(); + }); + connect(jobPtr.get(), + &NetJob::failed, + [this](QString reason) + { + abortable = false; + jobPtr.reset(); + emitFailed(reason); + }); + connect(jobPtr.get(), + &NetJob::progress, + [this](qint64 current, qint64 total) + { + abortable = true; + setProgress(current, total); + }); + connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); + connect(jobPtr.get(), + &NetJob::aborted, + [this] + { + abortable = false; + jobPtr.reset(); + emitAborted(); + }); + + jobPtr->start(); + } + + void PackInstallTask::extractConfigs() + { + qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId(); + setStatus(tr("Extracting configs...")); + + QDir extractDir(m_stagingPath); + + QuaZip packZip(archivePath); + if (!packZip.open(QuaZip::mdUnzip)) + { + emitFailed(tr("Failed to open pack configs %1!").arg(archivePath)); + return; + } + + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), + QOverload::of(MMCZip::extractDir), + archivePath, + extractDir.absolutePath() + "/minecraft"); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [this]() { downloadMods(); }); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [this]() { emitAborted(); }); + m_extractFutureWatcher.setFuture(m_extractFuture); + } + + void PackInstallTask::downloadMods() + { + qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); + + QList optionalMods; + for (const auto& mod : m_version.mods) + { + if (mod.optional) + { + optionalMods.push_back(mod); + } + } + + // Select optional mods, if pack contains any + QList selectedMods; + if (!optionalMods.isEmpty()) + { + setStatus(tr("Selecting optional mods...")); + auto mods = m_support->chooseOptionalMods(m_version, optionalMods); + if (!mods.has_value()) + { + emitAborted(); + return; + } + selectedMods = mods.value(); + } + + setStatus(tr("Downloading mods...")); + + jarmods.clear(); + jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network())); + + QList blocked_mods; + for (const auto& mod : m_version.mods) + { + // skip non-client mods + if (!mod.client) + continue; + + // skip optional mods that were not selected + if (mod.optional && !selectedMods.contains(mod.name)) + continue; + + QString url; + switch (mod.download) + { + case DownloadType::Server: url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; break; + case DownloadType::Browser: + { + blocked_mods.append(mod); + continue; + } + case DownloadType::Direct: url = mod.url; break; + case DownloadType::Unknown: emitFailed(tr("Unknown download type: %1").arg(mod.download_raw)); return; + } + + QFileInfo fileName(mod.file); + auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); + + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract + || mod.type == ModType::ResourcePackExtract) + { + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToExtract.insert(entry->getFullPath(), mod); + + auto dl = Net::ApiDownload::makeCached(url, entry); + if (!mod.md5.isEmpty()) + { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); + } + jobPtr->addNetAction(dl); + } + else if (mod.type == ModType::Decomp) + { + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToDecomp.insert(entry->getFullPath(), mod); + + auto dl = Net::ApiDownload::makeCached(url, entry); + if (!mod.md5.isEmpty()) + { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); + } + jobPtr->addNetAction(dl); + } + else + { + auto relpath = getDirForModType(mod.type, mod.type_raw); + if (relpath == Q_NULLPTR) + continue; + + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + + auto dl = Net::ApiDownload::makeCached(url, entry); + if (!mod.md5.isEmpty()) + { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); + } + jobPtr->addNetAction(dl); + + auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + + if (mod.type == ModType::Forge) + { + auto ver = getComponentVersion("net.minecraftforge", mod.version); + if (ver) + { + componentsToInstall.insert("net.minecraftforge", ver); + continue; + } + + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + if (mod.type == ModType::Jar) + { + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + // Download after Forge handling, to avoid downloading Forge twice. + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; + } + } + if (!blocked_mods.isEmpty()) + { + QList mods; + + for (auto mod : blocked_mods) + { + BlockedMod blocked_mod; + blocked_mod.name = mod.file; + blocked_mod.websiteUrl = mod.url; + blocked_mod.hash = mod.md5; + blocked_mod.matched = false; + blocked_mod.localPath = ""; + + mods.append(blocked_mod); + } + + qWarning() << "Blocked mods found, displaying mod list"; + + BlockedModsDialog message_dialog( + nullptr, + tr("Blocked mods found"), + tr("The following files are not available for download in third party launchers.
" + "You will need to manually download them and add them to the instance."), + mods, + "md5"); + + message_dialog.setModal(true); + + if (message_dialog.exec()) + { + qDebug() << "Post dialog blocked mods list: " << mods; + for (auto blocked : mods) + { + if (!blocked.matched) + { + qDebug() << blocked.name << "was not matched to a local file, skipping copy"; + continue; + } + auto modIter = + std::find_if(blocked_mods.begin(), + blocked_mods.end(), + [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; }); + if (modIter == blocked_mods.end()) + continue; + auto mod = *modIter; + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract + || mod.type == ModType::ResourcePackExtract) + { + modsToExtract.insert(blocked.localPath, mod); + } + else if (mod.type == ModType::Decomp) + { + modsToDecomp.insert(blocked.localPath, mod); + } + else + { + auto relpath = getDirForModType(mod.type, mod.type_raw); + if (relpath == Q_NULLPTR) + continue; + + auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + + if (mod.type == ModType::Forge) + { + auto ver = getComponentVersion("net.minecraftforge", mod.version); + if (ver) + { + componentsToInstall.insert("net.minecraftforge", ver); + continue; + } + + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + if (mod.type == ModType::Jar) + { + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + modsToCopy[blocked.localPath] = path; + } + } + } + else + { + emitFailed(tr("Unknown download type: %1").arg("browser")); + return; + } + } + + connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); + connect(jobPtr.get(), + &NetJob::progress, + [this](qint64 current, qint64 total) + { + setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); + abortable = true; + setProgress(current, total); + }); + connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); + connect(jobPtr.get(), &NetJob::aborted, &PackInstallTask::emitAborted); + connect(jobPtr.get(), &NetJob::failed, &PackInstallTask::emitFailed); + + jobPtr->start(); + } + + void PackInstallTask::onModsDownloaded() + { + abortable = false; + + qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); + jobPtr.reset(); + + if (!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) + { + m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), + &PackInstallTask::extractMods, + this, + modsToExtract, + modsToDecomp, + modsToCopy); + connect(&m_modExtractFutureWatcher, + &QFutureWatcher::finished, + this, + &PackInstallTask::onModsExtracted); + connect(&m_modExtractFutureWatcher, + &QFutureWatcher::canceled, + this, + &PackInstallTask::emitAborted); + m_modExtractFutureWatcher.setFuture(m_modExtractFuture); + } + else + { + install(); + } + } + + void PackInstallTask::onModsExtracted() + { + qDebug() << "PackInstallTask::onModsExtracted: " << QThread::currentThreadId(); + if (m_modExtractFuture.result()) + { + install(); + } + else + { + emitFailed(tr("Failed to extract mods...")); + } + } + + bool PackInstallTask::extractMods(const QMap& toExtract, + const QMap& toDecomp, + const QMap& toCopy) + { + qDebug() << "PackInstallTask::extractMods: " << QThread::currentThreadId(); + + setStatus(tr("Extracting mods...")); + for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) + { + auto& modPath = iter.key(); + auto& mod = iter.value(); + + QString extractToDir; + if (mod.type == ModType::Extract) + { + extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw); + } + else if (mod.type == ModType::TexturePackExtract) + { + extractToDir = FS::PathCombine("texturepacks", "extracted"); + } + else if (mod.type == ModType::ResourcePackExtract) + { + extractToDir = FS::PathCombine("resourcepacks", "extracted"); + } + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir); + + QString folderToExtract = ""; + if (mod.type == ModType::Extract) + { + folderToExtract = mod.extractFolder; + static const QRegularExpression s_regex("^/"); + folderToExtract.remove(s_regex); + } + + qDebug() << "Extracting " + mod.file + " to " + extractToDir; + if (!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) + { + // assume error + return false; + } + } + + for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) + { + auto& modPath = iter.key(); + auto& mod = iter.value(); + auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); + + qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; + if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) + { + qWarning() << "Failed to extract" << mod.decompFile; + return false; + } + } + + for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) + { + auto& from = iter.key(); + auto& to = iter.value(); + + // If the file already exists, assume the mod is the correct copy - and remove + // the copy from the Configs.zip + QFileInfo fileInfo(to); + if (fileInfo.exists()) + { + if (!FS::deletePath(to)) + { + qWarning() << "Failed to delete" << to; + return false; + } + } + + FS::copy fileCopyOperation(from, to); + if (!fileCopyOperation()) + { + qWarning() << "Failed to copy" << from << "to" << to; + return false; + } + } + return true; + } + + void PackInstallTask::install() + { + qDebug() << "PackInstallTask::install: " << QThread::currentThreadId(); + setStatus(tr("Installing modpack")); + + auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + // Use a component to add libraries BEFORE Minecraft + if (!createLibrariesComponent(instance.instanceRoot(), components)) + { + emitFailed(tr("Failed to create libraries component")); + return; + } + + // Minecraft + components->setComponentVersion("net.minecraft", m_version.minecraft, true); + + // Loader + if (m_version.loader.type == QString("forge")) + { + auto version = getVersionForLoader("net.minecraftforge"); + if (version == Q_NULLPTR) + return; + + components->setComponentVersion("net.minecraftforge", version); + } + else if (m_version.loader.type == QString("neoforge")) + { + auto version = getVersionForLoader("net.neoforged"); + if (version == Q_NULLPTR) + return; + + components->setComponentVersion("net.neoforged", version); + } + else if (m_version.loader.type == QString("fabric")) + { + auto version = getVersionForLoader("net.fabricmc.fabric-loader"); + if (version == Q_NULLPTR) + return; + + components->setComponentVersion("net.fabricmc.fabric-loader", version); + } + else if (m_version.loader.type != QString()) + { + emitFailed(tr("Unknown loader type: ") + m_version.loader.type); + return; + } + + for (const auto& componentUid : componentsToInstall.keys()) + { + auto version = componentsToInstall.value(componentUid); + components->setComponentVersion(componentUid, version->versionId()); + } + + components->installJarMods(jarmods); + + // Use a component to fill in the rest of the data + // todo: use more detection + if (!createPackComponent(instance.instanceRoot(), components)) + { + emitFailed(tr("Failed to create pack component")); + return; + } + + components->saveNow(); + + instance.setName(name()); + instance.setIconKey(m_instIcon); + instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); + instanceSettings->resumeSave(); + + jarmods.clear(); + emitSucceeded(); + } + + static projt::meta::MetaVersion::Ptr getComponentVersion(const QString& uid, const QString& version) + { + return APPLICATION->metadataIndex()->loadVersionBlocking(uid, version); + } + +} // namespace ATLauncher diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h new file mode 100644 index 0000000000..f3ab5946e0 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * ======================================================================== */ + +#pragma once + +#include +#include "ATLPackManifest.h" + +#include "InstanceTask.h" +#include "meta/Version.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/NetJob.h" +#include "settings/INISettingsObject.h" + +#include +#include + +namespace ATLauncher +{ + + enum class InstallMode + { + Install, + Reinstall, + Update, + }; + + class UserInteractionSupport + { + public: + /** + * Requests a user interaction to select which optional mods should be installed. + */ + virtual std::optional> chooseOptionalMods(const PackVersion& version, + QList mods) = 0; + + /** + * Requests a user interaction to select a component version from a given version list + * and constrained to a given Minecraft version. + */ + virtual QString chooseVersion(projt::meta::MetaVersionList::Ptr vlist, QString minecraftVersion) = 0; + + /** + * Requests a user interaction to display a message. + */ + virtual void displayMessage(QString message) = 0; + + virtual ~UserInteractionSupport() = default; + }; + + class PackInstallTask : public InstanceTask + { + Q_OBJECT + + public: + explicit PackInstallTask(UserInteractionSupport* support, + QString packName, + QString version, + InstallMode installMode = InstallMode::Install); + virtual ~PackInstallTask() + { + delete m_support; + } + + bool canAbort() const override + { + return true; + } + bool abort() override; + + protected: + virtual void executeTask() override; + + private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + void onDownloadAborted(); + + void onModsDownloaded(); + void onModsExtracted(); + + private: + QString getDirForModType(ModType type, QString raw); + QString getVersionForLoader(QString uid); + QString detectLibrary(const VersionLibrary& library); + + bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); + bool createPackComponent(QString instanceRoot, std::shared_ptr profile); + + void deleteExistingFiles(); + void installConfigs(); + void extractConfigs(); + void downloadMods(); + bool extractMods(const QMap& toExtract, + const QMap& toDecomp, + const QMap& toCopy); + void install(); + + private: + UserInteractionSupport* m_support; + + bool abortable = false; + + NetJob::Ptr jobPtr; + std::shared_ptr response = std::make_shared(); + + InstallMode m_install_mode; + QString m_pack_name; + QString m_pack_safe_name; + QString m_version_name; + PackVersion m_version; + + QMap modsToExtract; + QMap modsToDecomp; + QMap modsToCopy; + + QString archivePath; + QStringList jarmods; + projt::meta::MetaVersion::Ptr minecraftVersion; + QMap componentsToInstall; + + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + + QFuture m_modExtractFuture; + QFutureWatcher m_modExtractFutureWatcher; + }; + +} // namespace ATLauncher diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp new file mode 100644 index 0000000000..35cc7dec17 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * ======================================================================== */ + +#include "ATLPackManifest.h" + +#include "Json.h" + +static ATLauncher::DownloadType parseDownloadType(QString rawType) +{ + if (rawType == QString("server")) + { + return ATLauncher::DownloadType::Server; + } + else if (rawType == QString("browser")) + { + return ATLauncher::DownloadType::Browser; + } + else if (rawType == QString("direct")) + { + return ATLauncher::DownloadType::Direct; + } + + return ATLauncher::DownloadType::Unknown; +} + +static ATLauncher::ModType parseModType(QString rawType) +{ + // See https://wiki.atlauncher.com/mod_types + if (rawType == QString("root")) + { + return ATLauncher::ModType::Root; + } + else if (rawType == QString("forge")) + { + return ATLauncher::ModType::Forge; + } + else if (rawType == QString("jar")) + { + return ATLauncher::ModType::Jar; + } + else if (rawType == QString("mods")) + { + return ATLauncher::ModType::Mods; + } + else if (rawType == QString("flan")) + { + return ATLauncher::ModType::Flan; + } + else if (rawType == QString("dependency") || rawType == QString("depandency")) + { + return ATLauncher::ModType::Dependency; + } + else if (rawType == QString("ic2lib")) + { + return ATLauncher::ModType::Ic2Lib; + } + else if (rawType == QString("denlib")) + { + return ATLauncher::ModType::DenLib; + } + else if (rawType == QString("coremods")) + { + return ATLauncher::ModType::Coremods; + } + else if (rawType == QString("mcpc")) + { + return ATLauncher::ModType::MCPC; + } + else if (rawType == QString("plugins")) + { + return ATLauncher::ModType::Plugins; + } + else if (rawType == QString("extract")) + { + return ATLauncher::ModType::Extract; + } + else if (rawType == QString("decomp")) + { + return ATLauncher::ModType::Decomp; + } + else if (rawType == QString("texturepack")) + { + return ATLauncher::ModType::TexturePack; + } + else if (rawType == QString("resourcepack")) + { + return ATLauncher::ModType::ResourcePack; + } + else if (rawType == QString("shaderpack")) + { + return ATLauncher::ModType::ShaderPack; + } + else if (rawType == QString("texturepackextract")) + { + return ATLauncher::ModType::TexturePackExtract; + } + else if (rawType == QString("resourcepackextract")) + { + return ATLauncher::ModType::ResourcePackExtract; + } + else if (rawType == QString("millenaire")) + { + return ATLauncher::ModType::Millenaire; + } + + return ATLauncher::ModType::Unknown; +} + +static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj) +{ + p.type = Json::requireString(obj, "type"); + p.choose = Json::ensureBoolean(obj, QString("choose"), false); + + auto metadata = Json::requireObject(obj, "metadata"); + p.latest = Json::ensureBoolean(metadata, QString("latest"), false); + p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false); + + // Minecraft Forge + if (p.type == "forge") + { + p.version = Json::ensureString(metadata, "version", ""); + } + + // Fabric Loader + if (p.type == "fabric") + { + p.version = Json::ensureString(metadata, "loader", ""); + } +} + +static void loadVersionLibrary(ATLauncher::VersionLibrary& p, QJsonObject& obj) +{ + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::requireString(obj, "md5"); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.server = Json::ensureString(obj, "server", ""); +} + +static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj) +{ + p.filesize = Json::requireInteger(obj, "filesize"); + p.sha1 = Json::requireString(obj, "sha1"); +} + +static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj) +{ + p.name = Json::requireString(obj, "name"); + p.version = Json::requireString(obj, "version"); + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::ensureString(obj, "md5", ""); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.type_raw = Json::requireString(obj, "type"); + p.type = parseModType(p.type_raw); + + // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge" + // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some + // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best + // it can). + if (p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) + { + p.type_raw = "forge"; + p.type = ATLauncher::ModType::Forge; + } + + if (obj.contains("extractTo")) + { + p.extractTo_raw = Json::requireString(obj, "extractTo"); + p.extractTo = parseModType(p.extractTo_raw); + p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/"); + } + + if (obj.contains("decompType")) + { + p.decompType_raw = Json::requireString(obj, "decompType"); + p.decompType = parseModType(p.decompType_raw); + p.decompFile = Json::requireString(obj, "decompFile"); + } + + p.description = Json::ensureString(obj, QString("description"), ""); + p.optional = Json::ensureBoolean(obj, QString("optional"), false); + p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); + p.selected = Json::ensureBoolean(obj, QString("selected"), false); + p.hidden = Json::ensureBoolean(obj, QString("hidden"), false); + p.library = Json::ensureBoolean(obj, QString("library"), false); + p.group = Json::ensureString(obj, QString("group"), ""); + if (obj.contains("depends")) + { + auto dependsArr = Json::requireArray(obj, "depends"); + for (const auto depends : dependsArr) + { + p.depends.append(Json::requireString(depends)); + } + } + p.colour = Json::ensureString(obj, QString("colour"), ""); + p.warning = Json::ensureString(obj, QString("warning"), ""); + + p.client = Json::ensureBoolean(obj, QString("client"), false); + + // computed + p.effectively_hidden = p.hidden || p.library; +} + +static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) +{ + m.install = Json::ensureString(obj, "install", ""); + m.update = Json::ensureString(obj, "update", ""); +} + +static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj) +{ + m.mainClass = Json::ensureString(obj, "mainClass", ""); + m.depends = Json::ensureString(obj, "depends", ""); +} + +static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj) +{ + a.arguments = Json::ensureString(obj, "arguments", ""); + a.depends = Json::ensureString(obj, "depends", ""); +} + +static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj) +{ + k.base = Json::requireString(obj, "base"); + k.target = Json::requireString(obj, "target"); +} + +static void loadVersionKeeps(ATLauncher::VersionKeeps& k, QJsonObject& obj) +{ + if (obj.contains("files")) + { + auto files = Json::requireArray(obj, "files"); + for (const auto keepRaw : files) + { + auto keepObj = Json::requireObject(keepRaw); + ATLauncher::VersionKeep keep; + loadVersionKeep(keep, keepObj); + k.files.append(keep); + } + } + + if (obj.contains("folders")) + { + auto folders = Json::requireArray(obj, "folders"); + for (const auto keepRaw : folders) + { + auto keepObj = Json::requireObject(keepRaw); + ATLauncher::VersionKeep keep; + loadVersionKeep(keep, keepObj); + k.folders.append(keep); + } + } +} + +static void loadVersionDelete(ATLauncher::VersionDelete& d, QJsonObject& obj) +{ + d.base = Json::requireString(obj, "base"); + d.target = Json::requireString(obj, "target"); +} + +static void loadVersionDeletes(ATLauncher::VersionDeletes& d, QJsonObject& obj) +{ + if (obj.contains("files")) + { + auto files = Json::requireArray(obj, "files"); + for (const auto deleteRaw : files) + { + auto deleteObj = Json::requireObject(deleteRaw); + ATLauncher::VersionDelete versionDelete; + loadVersionDelete(versionDelete, deleteObj); + d.files.append(versionDelete); + } + } + + if (obj.contains("folders")) + { + auto folders = Json::requireArray(obj, "folders"); + for (const auto deleteRaw : folders) + { + auto deleteObj = Json::requireObject(deleteRaw); + ATLauncher::VersionDelete versionDelete; + loadVersionDelete(versionDelete, deleteObj); + d.folders.append(versionDelete); + } + } +} + +void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj) +{ + v.version = Json::requireString(obj, "version"); + v.minecraft = Json::requireString(obj, "minecraft"); + v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false); + + if (obj.contains("mainClass")) + { + auto main = Json::requireObject(obj, "mainClass"); + loadVersionMainClass(v.mainClass, main); + } + + if (obj.contains("extraArguments")) + { + auto arguments = Json::requireObject(obj, "extraArguments"); + loadVersionExtraArguments(v.extraArguments, arguments); + } + + if (obj.contains("loader")) + { + auto loader = Json::requireObject(obj, "loader"); + loadVersionLoader(v.loader, loader); + } + + if (obj.contains("libraries")) + { + auto libraries = Json::requireArray(obj, "libraries"); + for (const auto libraryRaw : libraries) + { + auto libraryObj = Json::requireObject(libraryRaw); + ATLauncher::VersionLibrary target; + loadVersionLibrary(target, libraryObj); + v.libraries.append(target); + } + } + + if (obj.contains("mods")) + { + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) + { + auto modObj = Json::requireObject(modRaw); + ATLauncher::VersionMod mod; + loadVersionMod(mod, modObj); + v.mods.append(mod); + } + } + + if (obj.contains("configs")) + { + auto configsObj = Json::requireObject(obj, "configs"); + loadVersionConfigs(v.configs, configsObj); + } + + auto colourObj = Json::ensureObject(obj, "colours"); + for (const auto& key : colourObj.keys()) + { + v.colours[key] = Json::requireString(colourObj.value(key), "colour"); + } + + auto warningsObj = Json::ensureObject(obj, "warnings"); + for (const auto& key : warningsObj.keys()) + { + v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); + } + + auto messages = Json::ensureObject(obj, "messages"); + loadVersionMessages(v.messages, messages); + + auto keeps = Json::ensureObject(obj, "keeps"); + loadVersionKeeps(v.keeps, keeps); + + auto deletes = Json::ensureObject(obj, "deletes"); + loadVersionDeletes(v.deletes, deletes); +} diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h new file mode 100644 index 0000000000..5b09621de1 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include + +namespace ATLauncher +{ + + enum class PackType + { + Public, + Private + }; + + enum class ModType + { + Root, + Forge, + Jar, + Mods, + Flan, + Dependency, + Ic2Lib, + DenLib, + Coremods, + MCPC, + Plugins, + Extract, + Decomp, + TexturePack, + ResourcePack, + ShaderPack, + TexturePackExtract, + ResourcePackExtract, + Millenaire, + Unknown + }; + + enum class DownloadType + { + Server, + Browser, + Direct, + Unknown + }; + + struct VersionLoader + { + QString type; + bool latest; + bool recommended; + bool choose; + + QString version; + }; + + struct VersionLibrary + { + QString url; + QString file; + QString server; + QString md5; + DownloadType download; + QString download_raw; + }; + + struct VersionMod + { + QString name; + QString version; + QString url; + QString file; + QString md5; + DownloadType download; + QString download_raw; + ModType type; + QString type_raw; + + ModType extractTo; + QString extractTo_raw; + QString extractFolder; + + ModType decompType; + QString decompType_raw; + QString decompFile; + + QString description; + bool optional; + bool recommended; + bool selected; + bool hidden; + bool library; + QString group; + QStringList depends; + QString colour; + QString warning; + + bool client; + + // computed + bool effectively_hidden; + }; + + struct VersionConfigs + { + int filesize; + QString sha1; + }; + + struct VersionMessages + { + QString install; + QString update; + }; + + struct VersionKeep + { + QString base; + QString target; + }; + + struct VersionKeeps + { + QList files; + QList folders; + }; + + struct VersionDelete + { + QString base; + QString target; + }; + + struct VersionDeletes + { + QList files; + QList folders; + }; + + struct PackVersionMainClass + { + QString mainClass; + QString depends; + }; + + struct PackVersionExtraArguments + { + QString arguments; + QString depends; + }; + + struct PackVersion + { + QString pack; + QString version; + QString minecraft; + bool noConfigs; + PackVersionMainClass mainClass; + PackVersionExtraArguments extraArguments; + + VersionLoader loader; + QList libraries; + QList mods; + VersionConfigs configs; + + QMap colours; + QMap warnings; + VersionMessages messages; + + VersionKeeps keeps; + VersionDeletes deletes; + }; + + void loadVersion(PackVersion& v, QJsonObject& obj); + +} // namespace ATLauncher diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp new file mode 100644 index 0000000000..ac1c2ceb05 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ATLShareCode.h" + +#include "Json.h" + +namespace ATLauncher +{ + + static void loadShareCodeMod(ShareCodeMod& m, QJsonObject& obj) + { + m.selected = Json::requireBoolean(obj, "selected"); + m.name = Json::requireString(obj, "name"); + } + + static void loadShareCode(ShareCode& c, QJsonObject& obj) + { + c.pack = Json::requireString(obj, "pack"); + c.version = Json::requireString(obj, "version"); + + auto mods = Json::requireObject(obj, "mods"); + auto optional = Json::requireArray(mods, "optional"); + for (const auto modRaw : optional) + { + auto modObj = Json::requireObject(modRaw); + ShareCodeMod mod; + loadShareCodeMod(mod, modObj); + c.mods.append(mod); + } + } + + void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj) + { + r.error = Json::requireBoolean(obj, "error"); + r.code = Json::requireInteger(obj, "code"); + + if (obj.contains("message") && !obj.value("message").isNull()) + r.message = Json::requireString(obj, "message"); + + if (!r.error) + { + auto dataRaw = Json::requireObject(obj, "data"); + loadShareCode(r.data, dataRaw); + } + } + +} // namespace ATLauncher diff --git a/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h new file mode 100644 index 0000000000..c49153af67 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/atlauncher/ATLShareCode.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +namespace ATLauncher +{ + + struct ShareCodeMod + { + bool selected; + QString name; + }; + + struct ShareCode + { + QString pack; + QString version; + QList mods; + }; + + struct ShareCodeResponse + { + bool error; + int code; + QString message; + ShareCode data; + }; + + void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj); + +} // namespace ATLauncher diff --git a/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp new file mode 100644 index 0000000000..e526dde959 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.cpp @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "FileResolvingTask.h" +#include + +#include "Application.h" +#include "Json.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" + +#include "modplatform/modrinth/ModrinthPackIndex.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +static const FlameAPI flameAPI; +static ModrinthAPI modrinthAPI; + +Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess) +{} + +bool Flame::FileResolvingTask::abort() +{ + bool aborted = true; + if (m_task) + { + aborted = m_task->abort(); + } + return aborted ? Task::abort() : false; +} + +void Flame::FileResolvingTask::executeTask() +{ + if (m_manifest.files.isEmpty()) + { // no file to resolve so leave it empty and emit success immediately + emitSucceeded(); + return; + } + setStatus(tr("Resolving mod IDs...")); + setProgress(0, 3); + m_result.reset(new QByteArray()); + + QStringList fileIds; + for (auto file : m_manifest.files) + { + fileIds.push_back(QString::number(file.fileId)); + } + m_task = flameAPI.getFiles(fileIds, m_result); + + auto step_progress = std::make_shared(); + connect(m_task.get(), + &Task::succeeded, + this, + [this, step_progress]() + { + step_progress->state = TaskStepState::Succeeded; + stepProgress(*step_progress); + netJobFinished(); + }); + connect(m_task.get(), + &Task::failed, + this, + [this, step_progress](QString reason) + { + step_progress->state = TaskStepState::Failed; + stepProgress(*step_progress); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), + &Task::progress, + this, + [this, step_progress](qint64 current, qint64 total) + { + qDebug() << "Resolve slug progress" << current << total; + step_progress->update(current, total); + stepProgress(*step_progress); + }); + connect(m_task.get(), + &Task::status, + this, + [this, step_progress](QString status) + { + step_progress->status = status; + stepProgress(*step_progress); + }); + + m_task->start(); +} + +ModPlatform::ResourceType getResourceType(int classId) +{ + switch (classId) + { + case 17: // Worlds + return ModPlatform::ResourceType::World; + case 6: // Mods + return ModPlatform::ResourceType::Mod; + case 12: // Resource Packs + // return ModPlatform::ResourceType::ResourcePack; // not really a resourcepack + /* fallthrough */ + case 4546: // Customization + // return ModPlatform::ResourceType::ShaderPack; // not really a shaderPack + /* fallthrough */ + case 4471: // Modpacks + /* fallthrough */ + case 5: // Bukkit Plugins + /* fallthrough */ + case 4559: // Addons + /* fallthrough */ + default: return ModPlatform::ResourceType::Unknown; + } +} + +void Flame::FileResolvingTask::netJobFinished() +{ + setProgress(1, 3); + // job to check modrinth for blocked projects + QJsonDocument doc; + QJsonArray array; + + try + { + doc = Json::requireDocument(*m_result); + array = Json::requireArray(doc.object()["data"]); + } + catch (Json::JsonException& e) + { + qCritical() << "Non-JSON data returned from the CF API"; + qCritical() << e.cause(); + + emitFailed(tr("Invalid data returned from the API.")); + + return; + } + + QStringList hashes; + for (QJsonValueRef file : array) + { + try + { + auto obj = Json::requireObject(file); + auto version = FlameMod::loadIndexedPackVersion(obj); + auto fileid = version.fileId.toInt(); + Q_ASSERT(fileid != 0); + Q_ASSERT(m_manifest.files.contains(fileid)); + m_manifest.files[fileid].version = version; + auto url = QUrl(version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) + { + hashes.push_back(version.hash); + } + } + catch (Json::JsonException& e) + { + qCritical() << "Non-JSON data returned from the CF API"; + qCritical() << e.cause(); + + emitFailed(tr("Invalid data returned from the API.")); + + return; + } + } + if (hashes.isEmpty() || !APPLICATION->settings()->get("FallbackMRBlockedMods").toBool()) + { + getFlameProjects(); + return; + } + m_result.reset(new QByteArray()); + m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result); + (dynamic_cast(m_task.get()))->setAskRetry(false); + auto step_progress = std::make_shared(); + connect(m_task.get(), + &Task::succeeded, + this, + [this, step_progress]() + { + step_progress->state = TaskStepState::Succeeded; + stepProgress(*step_progress); + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *m_result; + + getFlameProjects(); + return; + } + + try + { + auto entries = Json::requireObject(doc); + for (auto& out : m_manifest.files) + { + auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) + { + try + { + auto entry = Json::requireObject(entries, out.version.hash); + + auto file = Modrinth::loadIndexedPackVersion(entry); + + out.version.downloadUrl = file.downloadUrl; + qDebug() << "Found alternative on modrinth " << out.version.fileName; + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + getFlameProjects(); + }); + connect(m_task.get(), + &Task::failed, + this, + [this, step_progress](QString reason) + { + step_progress->state = TaskStepState::Failed; + stepProgress(*step_progress); + getFlameProjects(); + }); + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), + &Task::progress, + this, + [this, step_progress](qint64 current, qint64 total) + { + qDebug() << "Resolve slug progress" << current << total; + step_progress->update(current, total); + stepProgress(*step_progress); + }); + connect(m_task.get(), + &Task::status, + this, + [this, step_progress](QString status) + { + step_progress->status = status; + stepProgress(*step_progress); + }); + + m_task->start(); +} + +void Flame::FileResolvingTask::getFlameProjects() +{ + setProgress(2, 3); + m_result.reset(new QByteArray()); + QStringList addonIds; + for (auto file : m_manifest.files) + { + addonIds.push_back(QString::number(file.projectId)); + } + + m_task = flameAPI.getProjects(addonIds, m_result); + + auto step_progress = std::make_shared(); + connect(m_task.get(), + &Task::succeeded, + this, + [this, step_progress] + { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*m_result, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *m_result; + return; + } + + try + { + QJsonArray entries; + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) + { + auto entry_obj = Json::requireObject(entry); + auto id = Json::requireInteger(entry_obj, "id"); + auto file = std::find_if(m_manifest.files.begin(), + m_manifest.files.end(), + [id](const Flame::File& file) { return file.projectId == id; }); + if (file == m_manifest.files.end()) + { + continue; + } + + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); + FlameMod::loadIndexedPack(file->pack, entry_obj); + file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId")); + if (file->resourceType == ModPlatform::ResourceType::World) + { + file->targetFolder = "saves"; + } + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + step_progress->state = TaskStepState::Succeeded; + stepProgress(*step_progress); + emitSucceeded(); + }); + + connect(m_task.get(), + &Task::failed, + this, + [this, step_progress](QString reason) + { + step_progress->state = TaskStepState::Failed; + stepProgress(*step_progress); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), + &Task::progress, + this, + [this, step_progress](qint64 current, qint64 total) + { + qDebug() << "Resolve slug progress" << current << total; + step_progress->update(current, total); + stepProgress(*step_progress); + }); + connect(m_task.get(), + &Task::status, + this, + [this, step_progress](QString status) + { + step_progress->status = status; + stepProgress(*step_progress); + }); + + m_task->start(); +} diff --git a/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h new file mode 100644 index 0000000000..e752c85d73 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FileResolvingTask.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ +#pragma once + +#include "PackManifest.h" +#include "tasks/Task.h" + +namespace Flame +{ + class FileResolvingTask : public Task + { + Q_OBJECT + public: + explicit FileResolvingTask(Flame::Manifest& toProcess); + virtual ~FileResolvingTask() = default; + + bool canAbort() const override + { + return true; + } + bool abort() override; + + const Flame::Manifest& getResults() const + { + return m_manifest; + } + + protected: + virtual void executeTask() override; + + protected slots: + void netJobFinished(); + + private: + void getFlameProjects(); + + private: /* data */ + Flame::Manifest m_manifest; + std::shared_ptr m_result; + Task::Ptr m_task; + }; +} // namespace Flame diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp new file mode 100644 index 0000000000..69d103d5bd --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.cpp @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * ======================================================================== */ + +#include "FlameAPI.h" +#include +#include +#include "BuildConfig.h" +#include "FlameModIndex.h" + +#include "Application.h" +#include "Json.h" +#include "modplatform/ModIndex.h" +#include "net/ApiDownload.h" +#include "net/ApiUpload.h" +#include "net/NetJob.h" + +Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shared_ptr response) +{ + auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray fingerprints_arr; + for (auto& fp : fingerprints) + { + fingerprints_arr.append(QString("%1").arg(fp)); + } + + body_obj["fingerprints"] = fingerprints_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction( + Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), response, body_raw)); + + return netJob; +} + +QString FlameAPI::getModFileChangelog(int modId, int fileId) +{ + QEventLoop lock; + QString changelog; + + auto netJob = makeShared(QString("Flame::FileChangelog"), APPLICATION->network()); + auto response = std::make_shared(); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2/changelog") + .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), + response)); + + QObject::connect(netJob.get(), + &NetJob::succeeded, + [&netJob, response, &changelog] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Flame::FileChangelog at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + + netJob->failed(parse_error.errorString()); + return; + } + + changelog = Json::ensureString(doc.object(), "data"); + }); + + QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); + + netJob->start(); + lock.exec(); + + return changelog; +} + +QString FlameAPI::getModDescription(int modId) +{ + QEventLoop lock; + QString description; + + auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); + auto response = std::make_shared(); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId)), + response)); + + QObject::connect(netJob.get(), + &NetJob::succeeded, + [&netJob, response, &description] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Flame::ModDescription at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + + netJob->failed(parse_error.errorString()); + return; + } + + description = Json::ensureString(doc.object(), "data"); + }); + + QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); + + netJob->start(); + lock.exec(); + + return description; +} + +Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr response) const +{ + auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray addons_arr; + for (auto& addonId : addonIds) + { + addons_arr.append(addonId); + } + + body_obj["modIds"] = addons_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction( + Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), response, body_raw)); + + QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); + + return netJob; +} + +Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr response) const +{ + auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray files_arr; + for (auto& fileId : fileIds) + { + files_arr.append(fileId); + } + + body_obj["fileIds"] = files_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction( + Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), response, body_raw)); + + QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); + + return netJob; +} + +Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const +{ + auto netJob = makeShared(QString("Flame::GetFile"), APPLICATION->network()); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId)), + response)); + + QObject::connect(netJob.get(), + &NetJob::failed, + [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; }); + + return netJob; +} + +QList FlameAPI::getSortingMethods() const +{ + // https://docs.curseforge.com/?python#tocS_ModsSearchSortField + return { { 1, "Featured", QObject::tr("Sort by Featured") }, + { 2, "Popularity", QObject::tr("Sort by Popularity") }, + { 3, "LastUpdated", QObject::tr("Sort by Last Updated") }, + { 4, "Name", QObject::tr("Sort by Name") }, + { 5, "Author", QObject::tr("Sort by Author") }, + { 6, "TotalDownloads", QObject::tr("Sort by Downloads") }, + { 7, "Category", QObject::tr("Sort by Category") }, + { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; +} + +Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatform::ResourceType type) +{ + auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type))), + response)); + QObject::connect(netJob.get(), + &Task::failed, + [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); + return netJob; +} + +Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +{ + return getCategories(response, ModPlatform::ResourceType::Mod); +} + +QList FlameAPI::loadModCategories(std::shared_ptr response) +{ + QList categories; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return categories; + } + + try + { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + for (auto val : arr) + { + auto cat = Json::requireObject(val); + auto id = Json::requireInteger(cat, "id"); + auto name = Json::requireString(cat, "name"); + categories.push_back({ name, QString::number(id) }); + } + } + catch (Json::JsonException& e) + { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + return categories; +}; + +std::optional FlameAPI::getLatestVersion(QList versions, + QList instanceLoaders, + ModPlatform::ModLoaderTypes modLoaders, + bool checkLoaders) +{ + static const auto noLoader = ModPlatform::ModLoaderType(0); + if (!checkLoaders) + { + std::optional ver; + for (auto file_tmp : versions) + { + if (!ver.has_value() || file_tmp.date > ver->date) + { + ver = file_tmp; + } + } + return ver; + } + QHash bestMatch; + auto checkVersion = + [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) + { + if (bestMatch.contains(loader)) + { + auto best = bestMatch.value(loader); + if (version.date > best.date) + { + bestMatch[loader] = version; + } + } + else + { + bestMatch[loader] = version; + } + }; + for (auto file_tmp : versions) + { + auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders); + if (loaders.isEmpty()) + { + checkVersion(file_tmp, noLoader); + } + else + { + for (auto loader : loaders) + { + checkVersion(file_tmp, loader); + } + } + } + // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on + // update + auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders); + currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader + + for (auto loader : currentLoaders) + { + if (bestMatch.contains(loader)) + { + auto bestForLoader = bestMatch.value(loader); + // awkward case where the mod has only two loaders and one of them is not specified + if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) + { + auto bestForNoLoader = bestMatch.value(noLoader); + if (bestForNoLoader.date > bestForLoader.date) + { + return bestForNoLoader; + } + } + return bestForLoader; + } + } + return {}; +} diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h new file mode 100644 index 0000000000..40197b9f51 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameAPI.h @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include "BuildConfig.h" +#include "Json.h" +#include "Version.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameModIndex.h" + +class FlameAPI : public ResourceAPI +{ + public: + QString getModFileChangelog(int modId, int fileId); + QString getModDescription(int modId); + + std::optional getLatestVersion(QList versions, + QList instanceLoaders, + ModPlatform::ModLoaderTypes fallback, + bool checkLoaders); + + Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); + Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; + Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const; + + static Task::Ptr getCategories(std::shared_ptr response, ModPlatform::ResourceType type); + static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadModCategories(std::shared_ptr response); + + QList getSortingMethods() const override; + + static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders) + { + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt); + } + + private: + static int getClassId(ModPlatform::ResourceType type) + { + switch (type) + { + default: + case ModPlatform::ResourceType::Mod: return 6; + case ModPlatform::ResourceType::ResourcePack: return 12; + case ModPlatform::ResourceType::ShaderPack: return 6552; + case ModPlatform::ResourceType::Modpack: return 4471; + case ModPlatform::ResourceType::DataPack: return 6945; + } + } + + static int getMappedModLoader(ModPlatform::ModLoaderType loaders) + { + // https://docs.curseforge.com/?http#tocS_ModLoaderType + switch (loaders) + { + case ModPlatform::Forge: return 1; + case ModPlatform::Cauldron: return 2; + case ModPlatform::LiteLoader: return 3; + case ModPlatform::Fabric: return 4; + case ModPlatform::Quilt: return 5; + case ModPlatform::NeoForge: return 6; + case ModPlatform::DataPack: + case ModPlatform::Babric: + case ModPlatform::BTA: + case ModPlatform::LegacyFabric: + case ModPlatform::Ornithe: + case ModPlatform::Rift: break; // not supported + default: break; + } + return 0; + } + + static const QStringList getModLoaderStrings(const ModPlatform::ModLoaderTypes types) + { + QStringList l; + for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) + { + if (types & loader) + { + l << QString::number(getMappedModLoader(loader)); + } + } + return l; + } + + static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) + { + return "[" + getModLoaderStrings(types).join(',') + "]"; + } + + public: + std::optional getSearchURL(SearchArgs const& args) const override + { + QStringList get_arguments; + get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); + get_arguments.append(QString("index=%1").arg(args.offset)); + get_arguments.append("pageSize=25"); + if (args.search.has_value()) + get_arguments.append(QString("searchFilter=%1").arg(args.search.value())); + if (args.sorting.has_value()) + get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); + get_arguments.append("sortOrder=desc"); + if (args.loaders.has_value()) + { + ModPlatform::ModLoaderTypes loaders = args.loaders.value(); + loaders &= ~ModPlatform::ModLoaderType::DataPack; + if (loaders != 0) + get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders))); + } + if (args.categoryIds.has_value() && !args.categoryIds->empty()) + get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); + + if (args.versions.has_value() && !args.versions.value().empty()) + get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); + + return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&'); + } + + std::optional getVersionsURL(VersionSearchArgs const& args) const override + { + auto addonId = args.pack->addonId.toString(); + QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); + + if (args.mcVersions.has_value()) + url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); + + if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack + && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) + { + int mappedModLoader = + getMappedModLoader(static_cast(static_cast(args.loaders.value()))); + url += QString("&modLoaderType=%1").arg(mappedModLoader); + } + return url; + } + + QJsonArray documentToArray(QJsonDocument& obj) const override + { + return Json::ensureArray(obj.object(), "data"); + } + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override + { + FlameMod::loadIndexedPack(m, obj); + } + ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, + ModPlatform::ResourceType resourceType) const override + { + auto arr = FlameMod::loadIndexedPackVersion(obj); + if (resourceType != ModPlatform::ResourceType::TexturePack) + { + return arr; + } + + // Filter texture packs based on Minecraft version compatibility + // Texture packs with the old format (pre-1.6) use a different structure + // than resource packs (1.6+). This filtering ensures we only show + // compatible texture packs for older Minecraft versions. + auto const& mc_versions = arr.mcVersion; + + if (mc_versions.isEmpty()) + { + // No version info available - allow it through + return arr; + } + + // Check if any of the supported versions is 1.6 or older (texture pack era) + bool hasOldVersion = std::any_of(mc_versions.constBegin(), + mc_versions.constEnd(), + [](auto const& mc_version) { return Version(mc_version) <= Version("1.6"); }); + + if (hasOldVersion) + { + return arr; + } + + // Version 1.6+ uses resource packs, not texture packs + return {}; + }; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override + { + FlameMod::loadBody(m); + } + + private: + std::optional getInfoURL(QString const& id) const override + { + return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); + } + std::optional getDependencyURL(DependencySearchArgs const& args) const override + { + auto addonId = args.dependency.addonId.toString(); + auto url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000&gameVersion=%2") + .arg(addonId, args.mcVersion.toString()); + if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader)) + { + int mappedModLoader = + getMappedModLoader(static_cast(static_cast(args.loader))); + url += QString("&modLoaderType=%1").arg(mappedModLoader); + } + return url; + } +}; diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp new file mode 100644 index 0000000000..eaa3e6e830 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "FlameCheckUpdate.h" +#include "Application.h" +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include +#include + +#include "Json.h" + +#include "QObjectPtr.h" +#include "ResourceDownloadTask.h" + +#include "minecraft/mod/tasks/GetModDependenciesTask.hpp" + +#include "modplatform/ModIndex.h" +#include "net/ApiDownload.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +static FlameAPI api; + +bool FlameCheckUpdate::abort() +{ + bool result = false; + if (m_task && m_task->canAbort()) + { + result = m_task->abort(); + } + Task::abort(); + return result; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void FlameCheckUpdate::executeTask() +{ + setStatus(tr("Preparing resources for CurseForge...")); + + auto netJob = new NetJob("Get latest versions", APPLICATION->network()); + connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods); + + connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress); + connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); + connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails); + for (auto* resource : m_resources) + { + auto project = std::make_shared(); + project->addonId = resource->metadata()->project_id.toString(); + auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions }); + if (!versionsUrlOptional.has_value()) + continue; + + auto response = std::make_shared(); + auto task = Net::ApiDownload::makeByteArray(versionsUrlOptional.value(), response); + + connect(task.get(), + &Task::succeeded, + this, + [this, resource, response] { getLatestVersionCallback(resource, response); }); + netJob->addNetAction(task); + } + m_task.reset(netJob); + m_task->start(); +} + +void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ptr response) +{ + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + // Fake pack with the necessary info to pass to the download task :) + auto pack = std::make_shared(); + pack->name = resource->name(); + pack->slug = resource->metadata()->slug; + pack->addonId = resource->metadata()->project_id; + pack->provider = ModPlatform::ResourceProvider::FLAME; + try + { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + FlameMod::loadIndexedPackVersions(*pack.get(), arr); + } + catch (Json::JsonException& e) + { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + auto latest_ver = + api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); + + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); + + if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) + { + QString reason; + if (dynamic_cast(resource) != nullptr) + reason = tr("No valid version found for this resource. It's probably unavailable for the current game " + "version / mod loader."); + else + reason = + tr("No valid version found for this resource. It's probably unavailable for the current game version."); + + emit checkFailed(resource, reason); + return; + } + + if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) + { + m_blocked[resource] = latest_ver->fileId.toString(); + return; + } + + if (!latest_ver->hash.isEmpty() + && (resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) + { + auto old_version = resource->metadata()->version_number; + if (old_version.isEmpty()) + { + if (resource->status() == ResourceStatus::NOT_INSTALLED) + old_version = tr("Not installed"); + else + old_version = tr("Unknown"); + } + + auto download_task = makeShared(pack, latest_ver.value(), m_resourceModel); + m_updates.emplace_back(pack->name, + resource->metadata()->hash, + old_version, + latest_ver->version, + latest_ver->version_type, + api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()), + ModPlatform::ResourceProvider::FLAME, + download_task, + resource->enabled()); + } + m_deps.append(std::make_shared(pack, latest_ver.value())); +} + +void FlameCheckUpdate::collectBlockedMods() +{ + QStringList addonIds; + QHash quickSearch; + for (auto const& resource : m_blocked.keys()) + { + auto addonId = resource->metadata()->project_id.toString(); + addonIds.append(addonId); + quickSearch[addonId] = resource; + } + + auto response = std::make_shared(); + Task::Ptr projTask; + + if (addonIds.isEmpty()) + { + emitSucceeded(); + return; + } + else if (addonIds.size() == 1) + { + projTask = api.getProject(*addonIds.begin(), response); + } + else + { + projTask = api.getProjects(addonIds, response); + } + + connect(projTask.get(), + &Task::succeeded, + this, + [this, response, addonIds, quickSearch] + { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Flame projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try + { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) + { + auto entry_obj = Json::requireObject(entry); + + auto id = QString::number(Json::requireInteger(entry_obj, "id")); + + auto resource = quickSearch.find(id).value(); + + ModPlatform::IndexedPack pack; + try + { + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); + + FlameMod::loadIndexedPack(pack, entry_obj); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); + emit checkFailed( + resource, + tr("Resource has a new update available, but is not downloadable using CurseForge."), + recover_url); + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error + connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress); + connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); + connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails); + m_task.reset(projTask); + m_task->start(); +} \ No newline at end of file diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h new file mode 100644 index 0000000000..69f963328b --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameCheckUpdate.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "modplatform/CheckUpdateTask.h" + +class FlameCheckUpdate : public CheckUpdateTask +{ + Q_OBJECT + + public: + FlameCheckUpdate(QList& resources, + std::list& mcVersions, + QList loadersList, + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) + {} + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + private slots: + void getLatestVersionCallback(Resource* resource, std::shared_ptr response); + void collectBlockedMods(); + + private: + Task::Ptr m_task = nullptr; + + QHash m_blocked; +}; diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp new file mode 100644 index 0000000000..8786509646 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -0,0 +1,934 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "FlameInstanceCreationTask.h" + +#include "QObjectPtr.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.hpp" +#include "modplatform/flame/FileResolvingTask.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/flame/PackManifest.h" + +#include "Application.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "Json.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "modplatform/helpers/OverrideUtils.h" + +#include "settings/INISettingsObject.h" + +#include "tasks/ConcurrentTask.h" +#include "ui/dialogs/BlockedModsDialog.h" +#include "ui/dialogs/CustomMessageBox.h" + +#include +#include + +#include "meta/Index.hpp" +#include "HardwareInfo.h" +#include "minecraft/World.h" +#include "minecraft/mod/tasks/LocalResourceParse.hpp" +#include "net/ApiDownload.h" +#include "ui/pages/modplatform/OptionalModDialog.h" + +static const FlameAPI api; + +bool FlameCreationTask::abort() +{ + if (!canAbort()) + return false; + + if (m_processUpdateFileInfoJob) + m_processUpdateFileInfoJob->abort(); + if (m_filesJob) + m_filesJob->abort(); + if (m_modIdResolver) + m_modIdResolver->abort(); + + return InstanceCreationTask::abort(); +} + +bool FlameCreationTask::updateInstance() +{ + auto instance_list = APPLICATION->instances(); + + // Note: Duplicate modpack detection uses managed name or instance ID lookup. + // If multiple installations exist, the first match is updated. + InstancePtr inst; + if (auto original_id = originalInstanceID(); !original_id.isEmpty()) + { + inst = instance_list->getInstanceById(original_id); + Q_ASSERT(inst); + } + else + { + // Duplicate Detection: Check for duplicates before assuming + auto all_instances = instance_list->getAllInstancesByManagedName(originalName()); + + if (all_instances.size() > 1) + { + emitFailed(tr("Multiple instances found for this modpack. Please update the specific instance you want to " + "modify to avoid ambiguity.")); + return false; + } + + if (all_instances.size() == 1) + { + inst = all_instances.first(); + } + else + { + // Fallback to name-based lookup if not found by managed ID + inst = instance_list->getInstanceById(originalName()); + } + + if (!inst) + { + // New instance creation flow - return false to call createInstance() + return false; + } + } + + QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); + + try + { + Flame::loadManifest(m_pack, index_path); + } + catch (const JSONValidationError& e) + { + setError(tr("Could not understand pack manifest:\n") + e.cause()); + return false; + } + + auto version_id = inst->getManagedPackVersionName(); + auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : ""; + + if (shouldConfirmUpdate()) + { + auto should_update = askIfShouldUpdate(m_parent, version_str); + if (should_update == ShouldUpdate::SkipUpdating) + return false; + if (should_update == ShouldUpdate::Cancel) + { + m_abort = true; + return false; + } + } + + QDir old_inst_dir(inst->instanceRoot()); + + QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "flame")); + QString old_index_path(FS::PathCombine(old_index_folder, "manifest.json")); + + QFileInfo old_index_file(old_index_path); + if (old_index_file.exists()) + { + Flame::Manifest old_pack; + Flame::loadManifest(old_pack, old_index_path); + + auto& old_files = old_pack.files; + + auto& files = m_pack.files; + + // Remove repeated files, we don't need to download them! + auto files_iterator = files.begin(); + while (files_iterator != files.end()) + { + auto const& file = files_iterator; + + auto old_file = old_files.find(file.key()); + if (old_file != old_files.end()) + { + // We found a match, but is it a different version? + if (old_file->fileId == file->fileId) + { + qDebug() << "Removed file at" << file->targetFolder << "with id" << file->fileId + << "from list of downloads"; + + old_files.remove(file.key()); + files_iterator = files.erase(files_iterator); + + if (files_iterator != files.begin()) + files_iterator--; + } + } + + files_iterator++; + } + + QDir old_minecraft_dir(inst->gameRoot()); + + // We will remove all the previous overrides, to prevent duplicate files! + // Note: Overrides intentionally replace all files on update - this matches modpack author expectations. + auto old_overrides = Override::readOverrides("overrides", old_index_folder); + for (const auto& entry : old_overrides) + { + if (entry.isEmpty()) + continue; + + // Skip removal of .disabled files (user-disabled mods should be preserved) + if (entry.endsWith(".disabled", Qt::CaseInsensitive)) + { + qDebug() << "Preserving disabled mod:" << entry; + continue; + } + + qDebug() << "Scheduling" << entry << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry + ".disabled")); + } + + // Remove remaining old files (we need to do an API request to know which ids are which files...) + QStringList fileIds; + + for (auto& file : old_files) + { + fileIds.append(QString::number(file.fileId)); + } + + auto raw_response = std::make_shared(); + auto job = api.getFiles(fileIds, raw_response); + + QEventLoop loop; + + connect(job.get(), + &Task::succeeded, + this, + [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] + { + // Parse the API response + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*raw_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Flame files task at " + << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *raw_response; + return; + } + + try + { + QJsonArray entries; + if (fileIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) + { + auto entry_obj = Json::requireObject(entry); + + Flame::File file; + // We don't care about blocked mods, we just need local data to delete the file + file.version = FlameMod::loadIndexedPackVersion(entry_obj); + auto id = Json::requireInteger(entry_obj, "id"); + old_files.insert(id, file); + } + } + catch (Json::JsonException& e) + { + qCritical() << e.cause() << e.what(); + } + + // Delete the files + for (auto& file : old_files) + { + if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty()) + continue; + + QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName)); + qDebug() << "Scheduling" << relative_path << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); + if (relative_path.endsWith(".disabled")) + { // remove it if it was enabled/disabled by user + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path.chopped(9))); + } + else + { + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path + ".disabled")); + } + } + }); + connect(job.get(), + &Task::failed, + this, + [](QString reason) { qCritical() << "Failed to get files: " << reason; }); + connect(job.get(), &Task::finished, &loop, &QEventLoop::quit); + + m_processUpdateFileInfoJob = job; + job->start(); + + loop.exec(); + + m_processUpdateFileInfoJob = nullptr; + } + else + { + // We don't have an old index file, so we may duplicate stuff! + auto dialog = CustomMessageBox::selectable( + m_parent, + tr("No index file."), + tr("We couldn't find a suitable index file for the older version. This may cause some " + "of the files to be duplicated. Do you want to continue?"), + QMessageBox::Warning, + QMessageBox::Ok | QMessageBox::Cancel); + + if (dialog->exec() == QDialog::DialogCode::Rejected) + { + m_abort = true; + return false; + } + } + + setOverride(true, inst->id()); + qDebug() << "Will override instance!"; + + m_instance = inst; + + // We let it go through the createInstance() stage, just with a couple modifications for updating + return false; +} + +QString FlameCreationTask::getVersionForLoader(QString uid, + QString loaderType, + QString loaderVersion, + QString mcVersion) +{ + if (loaderVersion == "recommended") + { + auto vlist = APPLICATION->metadataIndex()->component(uid); + if (!vlist) + { + setError(tr("Failed to get local metadata index for %1").arg(uid)); + return {}; + } + + if (!vlist->isLoaded()) + { + QEventLoop loadVersionLoop; + auto task = vlist->getLoadTask(); + connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); + if (!task->isRunning()) + task->start(); + + loadVersionLoop.exec(); + } + + for (auto version : vlist->allVersions()) + { + // first recommended build we find, we use. + if (!version->isStable()) + continue; + auto reqs = version->dependencies(); + + // filter by minecraft version, if the loader depends on a certain version. + // not all mod loaders depend on a given Minecraft version, so we won't do this + // filtering for those loaders. + if (loaderType == "forge" || loaderType == "neoforge") + { + auto iter = std::find_if(reqs.begin(), + reqs.end(), + [mcVersion](const projt::meta::ComponentDependency& req) + { return req.uid == "net.minecraft" && req.equalsVersion == mcVersion; }); + if (iter == reqs.end()) + continue; + } + return version->descriptor(); + } + + setError(tr("Failed to find version for %1 loader").arg(loaderType)); + return {}; + } + + if (loaderVersion.isEmpty()) + { + emitFailed(tr("No loader version set for modpack!")); + return {}; + } + + return loaderVersion; +} + +std::unique_ptr FlameCreationTask::createInstance() +{ + QEventLoop loop; + + QString parent_folder(FS::PathCombine(m_stagingPath, "flame")); + + try + { + QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); + if (!m_pack.is_loaded) + Flame::loadManifest(m_pack, index_path); + + // Keep index file in case we need it some other time (like when changing versions) + QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); + FS::ensureFilePathExists(new_index_place); + FS::move(index_path, new_index_place); + } + catch (const JSONValidationError& e) + { + setError(tr("Could not understand pack manifest:\n") + e.cause()); + return nullptr; + } + + if (!m_pack.overrides.isEmpty()) + { + QString overridePath = FS::PathCombine(m_stagingPath, m_pack.overrides); + if (QFile::exists(overridePath)) + { + // Create a list of overrides in "overrides.txt" inside flame/ + Override::createOverrides("overrides", parent_folder, overridePath); + + QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); + if (!FS::move(overridePath, mcPath)) + { + setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); + return nullptr; + } + } + else + { + logWarning(tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?") + .arg(m_pack.overrides)); + } + } + + QString loaderType; + QString loaderUid; + QString loaderVersion; + + for (auto& loader : m_pack.minecraft.modLoaders) + { + auto id = loader.id; + if (id.startsWith("neoforge-")) + { + id.remove("neoforge-"); + if (id.startsWith("1.20.1-")) + id.remove("1.20.1-"); // this is a mess for curseforge + loaderType = "neoforge"; + loaderUid = "net.neoforged"; + } + else if (id.startsWith("forge-")) + { + id.remove("forge-"); + loaderType = "forge"; + loaderUid = "net.minecraftforge"; + } + else if (id.startsWith("fabric-")) + { + id.remove("fabric-"); + loaderType = "fabric"; + loaderUid = "net.fabricmc.fabric-loader"; + } + else if (id.startsWith("quilt-")) + { + id.remove("quilt-"); + loaderType = "quilt"; + loaderUid = "org.quiltmc.quilt-loader"; + } + else + { + logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); + continue; + } + loaderVersion = id; + } + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + auto createdInstance = std::make_unique(m_globalSettings, instanceSettings, m_stagingPath); + auto& instance = *createdInstance; + auto mcVersion = m_pack.minecraft.version; + + // Hack to correct some 'special sauce'... + if (mcVersion.endsWith('.')) + { + static const QRegularExpression s_regex("[.]+$"); + mcVersion.remove(s_regex); + logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); + } + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", mcVersion, true); + if (!loaderType.isEmpty()) + { + auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion); + if (version.isEmpty()) + return nullptr; + components->setComponentVersion(loaderUid, version); + } + + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + else + { + if (m_pack.name.contains("Direwolf20")) + { + instance.setIconKey("steve"); + } + else if (m_pack.name.contains("FTB") || m_pack.name.contains("Feed The Beast")) + { + instance.setIconKey("ftb_logo"); + } + else + { + instance.setIconKey("flame"); + } + } + + int recommendedRAM = m_pack.minecraft.recommendedRAM; + + // only set memory if this is a fresh instance + if (m_instance == nullptr && recommendedRAM > 0) + { + const uint64_t sysMiB = HardwareInfo::totalRamMiB(); + const uint64_t max = sysMiB * 0.9; + if (static_cast(recommendedRAM) > max) + { + logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 " + "MiB to %2 MiB!") + .arg(recommendedRAM) + .arg(max)); + recommendedRAM = max; + } + + instance.settings()->set("OverrideMemory", true); + instance.settings()->set("MaxMemAlloc", recommendedRAM); + } + + QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); + QFileInfo jarmodsInfo(jarmodsPath); + if (jarmodsInfo.isDir()) + { + // install all the jar mods + qDebug() << "Found jarmods:"; + QDir jarmodsDir(jarmodsPath); + QStringList jarMods; + for (const auto& info : jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) + { + qDebug() << info.fileName(); + jarMods.push_back(info.absoluteFilePath()); + } + auto profile = instance.getPackProfile(); + profile->installJarMods(jarMods); + // nuke the original files + FS::deletePath(jarmodsPath); + } + + // Don't add managed info to packs without an ID (most likely imported from ZIP) + if (!m_managedId.isEmpty()) + instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version); + else + instance.setManagedPack("flame", "", name(), "", ""); + + instance.setName(name()); + + m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack)); + connect(m_modIdResolver.get(), + &Flame::FileResolvingTask::succeeded, + this, + [this, &loop] { idResolverSucceeded(loop); }); + connect(m_modIdResolver.get(), + &Flame::FileResolvingTask::failed, + [this, &loop](QString reason) + { + m_modIdResolver.reset(); + setError(tr("Unable to resolve mod IDs:\n") + reason); + loop.quit(); + }); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); + connect(m_modIdResolver.get(), + &Flame::FileResolvingTask::stepProgress, + this, + &FlameCreationTask::propagateStepProgress); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); + m_modIdResolver->start(); + + loop.exec(); + + bool did_succeed = getError().isEmpty(); + + // Update information of the already installed instance, if any. + if (m_instance && did_succeed) + { + setAbortable(false); + auto inst = m_instance.value(); + + inst->copyManagedPack(instance); + } + + if (did_succeed) + { + return createdInstance; + } + return nullptr; +} + +void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) +{ + auto results = m_modIdResolver->getResults().files; + + QStringList optionalFiles; + for (auto& result : results) + { + if (!result.required) + { + optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); + } + } + + if (!optionalFiles.empty()) + { + OptionalModDialog optionalModDialog(m_parent, optionalFiles); + if (optionalModDialog.exec() == QDialog::Rejected) + { + emitAborted(); + loop.quit(); + return; + } + + m_selectedOptionalMods = optionalModDialog.getResult(); + } + + // first check for blocked mods + QList blocked_mods; + auto anyBlocked = false; + for (const auto& result : results.values()) + { + if (result.resourceType != ModPlatform::ResourceType::Mod) + { + m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder)); + } + + // skip optional mods that were not selected + if (result.version.downloadUrl.isEmpty()) + { + BlockedMod blocked_mod; + blocked_mod.name = result.version.fileName; + blocked_mod.websiteUrl = + QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId)); + blocked_mod.hash = result.version.hash; + blocked_mod.matched = false; + blocked_mod.localPath = ""; + blocked_mod.targetFolder = result.targetFolder; + auto fileName = result.version.fileName; + fileName = FS::RemoveInvalidPathChars(fileName); + auto relpath = FS::PathCombine(result.targetFolder, fileName); + blocked_mod.disabled = !result.required && !m_selectedOptionalMods.contains(relpath); + + blocked_mods.append(blocked_mod); + + anyBlocked = true; + } + } + if (anyBlocked) + { + qWarning() << "Blocked mods found, displaying mod list"; + + BlockedModsDialog message_dialog( + m_parent, + tr("Blocked mods found"), + tr("The following files are not available for download in third party launchers.
" + "You will need to manually download them and add them to the instance."), + blocked_mods); + + message_dialog.setModal(true); + + if (message_dialog.exec()) + { + qDebug() << "Post dialog blocked mods list: " << blocked_mods; + copyBlockedMods(blocked_mods); + setupDownloadJob(loop); + } + else + { + m_modIdResolver.reset(); + setError("Canceled"); + loop.quit(); + } + } + else + { + setupDownloadJob(loop); + } +} + +void FlameCreationTask::setupDownloadJob(QEventLoop& loop) +{ + m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); + auto results = m_modIdResolver->getResults().files; + + for (const auto& result : results) + { + auto fileName = result.version.fileName; + fileName = FS::RemoveInvalidPathChars(fileName); + auto relpath = FS::PathCombine(result.targetFolder, fileName); + + if (!result.required && !m_selectedOptionalMods.contains(relpath)) + { + relpath += ".disabled"; + } + + relpath = FS::PathCombine("minecraft", relpath); + auto path = FS::PathCombine(m_stagingPath, relpath); + + if (!result.version.downloadUrl.isEmpty()) + { + qDebug() << "Will download" << result.version.downloadUrl << "to" << path; + auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); + m_filesJob->addNetAction(dl); + } + } + + connect(m_filesJob.get(), + &NetJob::finished, + this, + [this, &loop]() + { + m_filesJob.reset(); + validateOtherResources(loop); + }); + connect(m_filesJob.get(), + &NetJob::failed, + [this](QString reason) + { + m_filesJob.reset(); + setError(reason); + }); + connect(m_filesJob.get(), + &NetJob::progress, + this, + [this](qint64 current, qint64 total) + { + setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); + setProgress(current, total); + }); + connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); + + setStatus(tr("Downloading mods...")); + m_filesJob->start(); +} + +/// @brief copy the matched blocked mods to the instance staging area +/// @param blocked_mods list of the blocked mods and their matched paths +void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) +{ + setStatus(tr("Copying Blocked Mods...")); + setAbortable(false); + int i = 0; + int total = blocked_mods.length(); + setProgress(i, total); + for (auto const& mod : blocked_mods) + { + if (!mod.matched) + { + qDebug() << mod.name << "was not matched to a local file, skipping copy"; + continue; + } + + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); + if (mod.disabled) + destPath += ".disabled"; + + setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); + + qDebug() << "Will try to copy" << mod.localPath << "to" << destPath; + + if (mod.move) + { + if (!FS::move(mod.localPath, destPath)) + { + qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed"; + } + } + else + { + if (!FS::copy(mod.localPath, destPath)()) + { + qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; + } + } + + i++; + setProgress(i, total); + } + + setAbortable(true); +} + +void FlameCreationTask::validateOtherResources(QEventLoop& loop) +{ + qDebug() << "Validating whether other resources are in the right place"; + QStringList zipMods; + for (auto [fileName, targetFolder] : m_otherResources) + { + qDebug() << "Checking" << fileName << "..."; + auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); + + /// @brief check the target and move the the file + /// @return path where file can now be found + auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget) + { + if (targetFolder != realTarget) + { + qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget; + auto destPath = FS::PathCombine(m_stagingPath, "minecraft", realTarget, fileName); + qDebug() << "Moving" << localPath << "to" << destPath; + if (FS::move(localPath, destPath)) + { + return destPath; + } + } + else + { + qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder; + } + return localPath; + }; + + auto installWorld = [this](QString worldPath) + { + qDebug() << "Installing World from" << worldPath; + QFileInfo worldFileInfo(worldPath); + World w(worldFileInfo); + if (!w.isValid()) + { + qDebug() << "World at" << worldPath << "is not valid, skipping install."; + } + else + { + w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves")); + } + }; + + QFileInfo localFileInfo(localPath); + auto type = ResourceUtils::identify(localFileInfo); + + QString worldPath; + + switch (type) + { + case ModPlatform::ResourceType::Mod: + validatePath(fileName, targetFolder, "mods"); + zipMods.push_back(fileName); + break; + case ModPlatform::ResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); break; + case ModPlatform::ResourceType::TexturePack: validatePath(fileName, targetFolder, "texturepacks"); break; + case ModPlatform::ResourceType::DataPack: validatePath(fileName, targetFolder, "datapacks"); break; + case ModPlatform::ResourceType::ShaderPack: + // in theory flame API can't do this but who knows, that *may* change ? + // better to handle it if it *does* occur in the future + validatePath(fileName, targetFolder, "shaderpacks"); + break; + case ModPlatform::ResourceType::World: + worldPath = validatePath(fileName, targetFolder, "saves"); + installWorld(worldPath); + break; + case ModPlatform::ResourceType::Unknown: + /* fallthrough */ + default: + qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; + break; + } + } + // Generic metadata creation for supported resource types + auto task = makeShared("CreateModMetadata", + APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + auto results = m_modIdResolver->getResults().files; + + for (auto file : results) + { + QString indexParent; + switch (file.resourceType) + { + case ModPlatform::ResourceType::Mod: + // Skip if it looks like a zip but wasn't identified as a mod (e.g. valid resource pack that got + // confused, or invalid file) + if (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName)) + { + continue; + } + indexParent = "mods"; + break; + case ModPlatform::ResourceType::ResourcePack: indexParent = "resourcepacks"; break; + case ModPlatform::ResourceType::ShaderPack: indexParent = "shaderpacks"; break; + default: continue; + } + + auto folder = FS::PathCombine(m_stagingPath, "minecraft", indexParent, ".index"); + task->addTask(makeShared(folder, file.pack, file.version)); + } + + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); + m_processUpdateFileInfoJob = task; + task->start(); +} diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h new file mode 100644 index 0000000000..488b13e905 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "InstanceCreationTask.h" + +#include + +#include "minecraft/MinecraftInstance.h" + +#include "modplatform/flame/FileResolvingTask.h" + +#include "net/NetJob.h" + +#include "ui/dialogs/BlockedModsDialog.h" + +class FlameCreationTask final : public InstanceCreationTask +{ + Q_OBJECT + + public: + FlameCreationTask(const QString& staging_path, + SettingsObjectPtr global_settings, + QWidget* parent, + QString id, + QString version_id, + QString original_instance_id = {}) + : InstanceCreationTask(), + m_parent(parent), + m_managedId(std::move(id)), + m_managedVersionId(std::move(version_id)) + { + setStagingPath(staging_path); + setParentSettings(global_settings); + + m_original_instance_id = std::move(original_instance_id); + } + + bool abort() override; + + bool updateInstance() override; + std::unique_ptr createInstance() override; + + private slots: + void idResolverSucceeded(QEventLoop&); + void setupDownloadJob(QEventLoop&); + void copyBlockedMods(QList const& blocked_mods); + void validateOtherResources(QEventLoop& loop); + QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); + + private: + QWidget* m_parent = nullptr; + + shared_qobject_ptr m_modIdResolver; + Flame::Manifest m_pack; + + // Handle to allow aborting + Task::Ptr m_processUpdateFileInfoJob = nullptr; + NetJob::Ptr m_filesJob = nullptr; + + QString m_managedId, m_managedVersionId; + + QList> m_otherResources; + + std::optional m_instance; + + QStringList m_selectedOptionalMods; +}; diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp new file mode 100644 index 0000000000..7bf4917df8 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.cpp @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "FlameModIndex.h" + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" + +static FlameAPI api; + +void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + pack.addonId = Json::requireInteger(obj, "id"); + pack.provider = ModPlatform::ResourceProvider::FLAME; + pack.name = Json::requireString(obj, "name"); + pack.slug = Json::requireString(obj, "slug"); + pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); + pack.description = Json::ensureString(obj, "summary", ""); + + QJsonObject logo = Json::ensureObject(obj, "logo"); + pack.logoName = Json::ensureString(logo, "title"); + pack.logoUrl = Json::ensureString(logo, "thumbnailUrl"); + if (pack.logoUrl.isEmpty()) + { + pack.logoUrl = Json::ensureString(logo, "url"); + } + + auto authors = Json::ensureArray(obj, "authors"); + if (!authors.isEmpty()) + { + pack.authors.clear(); + for (auto authorIter : authors) + { + auto author = Json::requireObject(authorIter); + ModPlatform::ModpackAuthor packAuthor; + packAuthor.name = Json::requireString(author, "name"); + packAuthor.url = Json::requireString(author, "url"); + pack.authors.append(packAuthor); + } + } + + pack.extraDataLoaded = false; + loadURLs(pack, obj); +} + +void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + auto links_obj = Json::ensureObject(obj, "links"); + + pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); + if (pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); + if (pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); + if (pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + if (!pack.extraData.body.isEmpty()) + pack.extraDataLoaded = true; +} + +void FlameMod::loadBody(ModPlatform::IndexedPack& pack) +{ + pack.extraData.body = api.getModDescription(pack.addonId.toInt()); + + if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty()) + pack.extraDataLoaded = true; +} + +static QString enumToString(int hash_algorithm) +{ + switch (hash_algorithm) + { + default: + case 1: return "sha1"; + case 2: return "md5"; + } +} + +void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) +{ + QList unsortedVersions; + for (auto versionIter : arr) + { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj); + if (!file.addonId.isValid()) + file.addonId = pack.addonId; + + if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + unsortedVersions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool + { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + pack.versions = unsortedVersions; + pack.versionsLoaded = true; +} + +auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion +{ + auto versionArray = Json::requireArray(obj, "gameVersions"); + + ModPlatform::IndexedVersion file; + for (auto mcVer : versionArray) + { + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); + + file.side = ModPlatform::Side::NoSide; + if (auto loader = str.toLower(); loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "risugami") + file.loaders |= ModPlatform::Risugami; + else if (loader == "station-loader") + file.loaders |= ModPlatform::StationLoader; + else if (loader == "modloadermp") + file.loaders |= ModPlatform::ModLoaderMP; + else if (loader == "optifine") + file.loaders |= ModPlatform::Optifine; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + else if (loader == "server" || loader == "client") + { + if (file.side == ModPlatform::Side::NoSide) + file.side = ModPlatform::SideUtils::fromString(loader); + else if (file.side != ModPlatform::SideUtils::fromString(loader)) + file.side = ModPlatform::Side::UniversalSide; + } + } + + file.addonId = Json::requireInteger(obj, "modId"); + file.fileId = Json::requireInteger(obj, "id"); + file.date = Json::requireString(obj, "fileDate"); + file.version = Json::requireString(obj, "displayName"); + file.downloadUrl = Json::ensureString(obj, "downloadUrl"); + file.fileName = Json::requireString(obj, "fileName"); + file.fileName = FS::RemoveInvalidPathChars(file.fileName); + + ModPlatform::IndexedVersionType::VersionType ver_type; + switch (Json::requireInteger(obj, "releaseType")) + { + case 1: ver_type = ModPlatform::IndexedVersionType::VersionType::Release; break; + case 2: ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; break; + case 3: ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; break; + default: ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; + } + file.version_type = ModPlatform::IndexedVersionType(ver_type); + + auto hash_list = Json::ensureArray(obj, "hashes"); + for (auto h : hash_list) + { + auto hash_entry = Json::ensureObject(h); + auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME); + auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); + if (hash_types.contains(hash_algo)) + { + file.hash = Json::requireString(hash_entry, "value"); + file.hash_type = hash_algo; + break; + } + } + + auto dependencies = Json::ensureArray(obj, "dependencies"); + for (auto d : dependencies) + { + auto dep = Json::ensureObject(d); + ModPlatform::Dependency dependency; + dependency.addonId = Json::requireInteger(dep, "modId"); + switch (Json::requireInteger(dep, "relationType")) + { + case 1: // EmbeddedLibrary + dependency.type = ModPlatform::DependencyType::EMBEDDED; + break; + case 2: // OptionalDependency + dependency.type = ModPlatform::DependencyType::OPTIONAL; + break; + case 3: // RequiredDependency + dependency.type = ModPlatform::DependencyType::REQUIRED; + break; + case 4: // Tool + dependency.type = ModPlatform::DependencyType::TOOL; + break; + case 5: // Incompatible + dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; + break; + case 6: // Include + dependency.type = ModPlatform::DependencyType::INCLUDE; + break; + default: dependency.type = ModPlatform::DependencyType::UNKNOWN; break; + } + file.dependencies.append(dependency); + } + + if (load_changelog) + file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); + + return file; +} diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h new file mode 100644 index 0000000000..aebd7f2976 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlameModIndex.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "modplatform/ModIndex.h" + +#include "BaseInstance.h" + +namespace FlameMod +{ + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); + void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); + void loadBody(ModPlatform::IndexedPack& m); + void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); + ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false); +} // namespace FlameMod \ No newline at end of file diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp new file mode 100644 index 0000000000..4113ffd7ba --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -0,0 +1,566 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "FlamePackExportTask.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include "Application.h" +#include "Json.h" +#include "MMCZip.h" +#include "minecraft/PackProfile.h" +#include "minecraft/mod/ModFolderModel.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/helpers/HashUtils.h" +#include "tasks/Task.h" + +const QString FlamePackExportTask::TEMPLATE = "
  • {name}{authors}
  • \n"; +const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); + +FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options) + : m_options(std::move(options)), + m_gameRoot(m_options.instance->gameRoot()) +{} + +void FlamePackExportTask::executeTask() +{ + setStatus(tr("Searching for files...")); + setProgress(0, 5); + collectFiles(); +} + +bool FlamePackExportTask::abort() +{ + if (task) + { + task->abort(); + return true; + } + return false; +} + +void FlamePackExportTask::collectFiles() +{ + setAbortable(false); + QCoreApplication::processEvents(); + + m_files.clear(); + if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &m_files, m_options.filter)) + { + emitFailed(tr("Could not search for files")); + return; + } + + pendingHashes.clear(); + resolvedFiles.clear(); + + m_options.instance->loaderModList()->update(); + connect(m_options.instance->loaderModList().get(), + &ModFolderModel::updateFinished, + this, + &FlamePackExportTask::collectHashes); +} + +void FlamePackExportTask::collectHashes() +{ + setAbortable(true); + setStatus(tr("Finding file hashes...")); + setProgress(1, 5); + auto allMods = m_options.instance->loaderModList()->allMods(); + ConcurrentTask::Ptr hashingTask( + new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + task.reset(hashingTask); + for (const QFileInfo& file : m_files) + { + const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath()); + // require sensible file types + if (!std::any_of( + FILE_EXTENSIONS.begin(), + FILE_EXTENSIONS.end(), + [&relative](const QString& extension) + { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); })) + continue; + + if (relative.startsWith("resourcepacks/") && (relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) + { // is resourcepack + auto hashTask = Hashing::createHasher(file.absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); + connect(hashTask.get(), + &Hashing::Hasher::resultsReady, + [this, relative, file](QString hash) + { + if (m_state == Task::State::Running) + { + pendingHashes.insert(hash, + { relative, file.absoluteFilePath(), relative.endsWith(".zip") }); + } + }); + connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + hashingTask->addTask(hashTask); + continue; + } + + if (auto modIter = + std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); + modIter != allMods.end()) + { + const Mod* mod = *modIter; + if (!mod || mod->type() == ResourceType::FOLDER) + { + continue; + } + if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) + { + resolvedFiles.insert(mod->fileinfo().absoluteFilePath(), + { mod->metadata()->project_id.toInt(), + mod->metadata()->file_id.toInt(), + mod->enabled(), + true, + mod->metadata()->name, + mod->metadata()->slug, + mod->authors().join(", ") }); + continue; + } + + auto hashTask = + Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); + connect(hashTask.get(), + &Hashing::Hasher::resultsReady, + [this, mod](QString hash) + { + if (m_state == Task::State::Running) + { + pendingHashes.insert( + hash, + { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true }); + } + }); + connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + hashingTask->addTask(hashTask); + } + } + auto progressStep = std::make_shared(); + connect(hashingTask.get(), + &Task::finished, + this, + [this, progressStep] + { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest); + connect(hashingTask.get(), + &Task::failed, + this, + [this, progressStep](QString reason) + { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress); + + connect(hashingTask.get(), + &Task::progress, + this, + [this, progressStep](qint64 current, qint64 total) + { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(hashingTask.get(), + &Task::status, + this, + [this, progressStep](QString status) + { + progressStep->status = status; + stepProgress(*progressStep); + }); + connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); + hashingTask->start(); +} + +void FlamePackExportTask::makeApiRequest() +{ + if (pendingHashes.isEmpty()) + { + buildZip(); + return; + } + + setStatus(tr("Finding versions for hashes...")); + setProgress(2, 5); + auto response = std::make_shared(); + + QList fingerprints; + for (auto& murmur : pendingHashes.keys()) + { + fingerprints.push_back(murmur.toUInt()); + } + + task.reset(api.matchFingerprints(fingerprints, response)); + + connect(task.get(), + &Task::succeeded, + this, + [this, response] + { + QJsonParseError parseError{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at " + << parseError.offset << " reason: " << parseError.errorString(); + qWarning() << *response; + + emitFailed(parseError.errorString()); + return; + } + + try + { + auto docObj = Json::requireObject(doc); + auto dataObj = Json::requireObject(docObj, "data"); + auto dataArr = Json::requireArray(dataObj, "exactMatches"); + + if (dataArr.isEmpty()) + { + qWarning() << "No matches found for fingerprint search!"; + + getProjectsInfo(); + return; + } + for (auto match : dataArr) + { + auto matchObj = Json::ensureObject(match, {}); + auto fileObj = Json::ensureObject(matchObj, "file", {}); + + if (matchObj.isEmpty() || fileObj.isEmpty()) + { + qWarning() << "Fingerprint match is empty!"; + + return; + } + + auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt()); + auto mod = pendingHashes.find(fingerprint); + if (mod == pendingHashes.end()) + { + qWarning() << "Invalid fingerprint from the API response."; + continue; + } + + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name)); + if (Json::ensureBoolean(fileObj, "isAvailable", false, "isAvailable")) + resolvedFiles.insert(mod->path, + { Json::requireInteger(fileObj, "modId"), + Json::requireInteger(fileObj, "id"), + mod->enabled, + mod->isMod }); + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + pendingHashes.clear(); + getProjectsInfo(); + }); + connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); + connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); + task->start(); +} + +void FlamePackExportTask::getProjectsInfo() +{ + setStatus(tr("Finding project info from CurseForge...")); + setProgress(3, 5); + QStringList addonIds; + for (const auto& resolved : resolvedFiles) + { + if (resolved.slug.isEmpty()) + { + addonIds << QString::number(resolved.addonId); + } + } + + auto response = std::make_shared(); + Task::Ptr projTask; + + if (addonIds.isEmpty()) + { + buildZip(); + return; + } + else if (addonIds.size() == 1) + { + projTask = api.getProject(*addonIds.begin(), response); + } + else + { + projTask = api.getProjects(addonIds, response); + } + + connect(projTask.get(), + &Task::succeeded, + this, + [this, response, addonIds] + { + QJsonParseError parseError{}; + auto doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from CurseForge projects task at " + << parseError.offset << " reason: " << parseError.errorString(); + qWarning() << *response; + emitFailed(parseError.errorString()); + return; + } + + try + { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) + { + auto entryObj = Json::requireObject(entry); + + try + { + setStatus(tr("Parsing API response from CurseForge for '%1'...") + .arg(Json::requireString(entryObj, "name"))); + + ModPlatform::IndexedPack pack; + FlameMod::loadIndexedPack(pack, entryObj); + for (auto key : resolvedFiles.keys()) + { + auto val = resolvedFiles.value(key); + if (val.addonId == pack.addonId) + { + val.name = pack.name; + val.slug = pack.slug; + QStringList authors; + for (auto author : pack.authors) + authors << author.name; + + val.authors = authors.join(", "); + resolvedFiles[key] = val; + } + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } + catch (Json::JsonException& e) + { + qDebug() << e.cause(); + qDebug() << doc; + } + buildZip(); + }); + connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); + task.reset(projTask); + task->start(); +} + +void FlamePackExportTask::buildZip() +{ + setStatus(tr("Adding files...")); + setProgress(4, 5); + + auto zipTask = makeShared(m_options.output, m_gameRoot, m_files, "overrides/", true, false); + zipTask->addExtraFile("manifest.json", generateIndex()); + zipTask->addExtraFile("modlist.html", generateHTML()); + + QStringList exclude; + std::transform(resolvedFiles.keyBegin(), + resolvedFiles.keyEnd(), + std::back_insert_iterator(exclude), + [this](QString file) { return m_gameRoot.relativeFilePath(file); }); + zipTask->setExcludeFiles(exclude); + + auto progressStep = std::make_shared(); + connect(zipTask.get(), + &Task::finished, + this, + [this, progressStep] + { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(zipTask.get(), &Task::succeeded, this, &FlamePackExportTask::emitSucceeded); + connect(zipTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); + connect(zipTask.get(), + &Task::failed, + this, + [this, progressStep](QString reason) + { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(zipTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress); + + connect(zipTask.get(), + &Task::progress, + this, + [this, progressStep](qint64 current, qint64 total) + { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(zipTask.get(), + &Task::status, + this, + [this, progressStep](QString status) + { + progressStep->status = status; + stepProgress(*progressStep); + }); + task.reset(zipTask); + zipTask->start(); +} + +QByteArray FlamePackExportTask::generateIndex() +{ + QJsonObject obj; + obj["manifestType"] = "minecraftModpack"; + obj["manifestVersion"] = 1; + obj["name"] = m_options.name; + obj["version"] = m_options.version; + obj["author"] = m_options.author; + obj["overrides"] = "overrides"; + + QJsonObject version; + + auto profile = m_options.instance->getPackProfile(); + // collect all supported components + const ComponentPtr minecraft = profile->getComponent("net.minecraft"); + const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); + const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); + const ComponentPtr forge = profile->getComponent("net.minecraftforge"); + const ComponentPtr neoforge = profile->getComponent("net.neoforged"); + + // convert all available components to mrpack dependencies + if (minecraft != nullptr) + version["version"] = minecraft->m_version; + QString id; + if (quilt != nullptr) + id = "quilt-" + quilt->m_version; + else if (fabric != nullptr) + id = "fabric-" + fabric->m_version; + else if (forge != nullptr) + id = "forge-" + forge->m_version; + else if (neoforge != nullptr) + { + id = "neoforge-"; + if (minecraft->m_version == "1.20.1") + id += "1.20.1-"; + id += neoforge->m_version; + } + version["modLoaders"] = QJsonArray(); + if (!id.isEmpty()) + { + QJsonObject loader; + loader["id"] = id; + loader["primary"] = true; + version["modLoaders"] = QJsonArray({ loader }); + } + + if (m_options.recommendedRAM > 0) + version["recommendedRam"] = m_options.recommendedRAM; + + obj["minecraft"] = version; + + QJsonArray files; + for (auto mod : resolvedFiles) + { + QJsonObject file; + file["projectID"] = mod.addonId; + file["fileID"] = mod.version; + file["required"] = mod.enabled || !m_options.optionalFiles; + files << file; + } + obj["files"] = files; + + return QJsonDocument(obj).toJson(QJsonDocument::Compact); +} + +QByteArray FlamePackExportTask::generateHTML() +{ + QString content = ""; + for (auto mod : resolvedFiles) + { + if (mod.isMod) + { + content += + QString(TEMPLATE) + .replace("{name}", mod.name.toHtmlEscaped()) + .replace("{url}", + ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped()) + .replace("{authors}", + !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : ""); + } + } + content = "
      " + content + "
    "; + return content.toUtf8(); +} diff --git a/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h new file mode 100644 index 0000000000..2535960a38 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/FlamePackExportTask.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "MMCZip.h" +#include "minecraft/MinecraftInstance.h" +#include "modplatform/flame/FlameAPI.h" +#include "tasks/Task.h" + +struct FlamePackExportOptions +{ + QString name; + QString version; + QString author; + bool optionalFiles; + MinecraftInstancePtr instance; + QString output; + MMCZip::FilterFileFunction filter; + int recommendedRAM; +}; + +class FlamePackExportTask : public Task +{ + Q_OBJECT + public: + FlamePackExportTask(FlamePackExportOptions&& options); + + protected: + void executeTask() override; + bool abort() override; + + private: + static const QString TEMPLATE; + static const QStringList FILE_EXTENSIONS; + + // inputs + + struct ResolvedFile + { + int addonId; + int version; + bool enabled; + bool isMod; + + QString name; + QString slug; + QString authors; + }; + struct HashInfo + { + QString name; + QString path; + bool enabled; + bool isMod; + }; + + FlamePackExportOptions m_options; + QDir m_gameRoot; + + FlameAPI api; + + QFileInfoList m_files; + QMap pendingHashes{}; + QMap resolvedFiles{}; + Task::Ptr task; + + void collectFiles(); + void collectHashes(); + void makeApiRequest(); + void getProjectsInfo(); + void buildZip(); + + QByteArray generateIndex(); + QByteArray generateHTML(); +}; diff --git a/archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp new file mode 100644 index 0000000000..9bd57387d6 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "PackManifest.h" +#include "Json.h" + +static void loadFileV1(Flame::File& f, QJsonObject& file) +{ + f.projectId = Json::requireInteger(file, "projectID"); + f.fileId = Json::requireInteger(file, "fileID"); + f.required = Json::ensureBoolean(file, QString("required"), true); +} + +static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader) +{ + m.id = Json::requireString(modLoader, "id"); + m.primary = Json::ensureBoolean(modLoader, QString("primary"), false); +} + +static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) +{ + m.version = Json::requireString(minecraft, "version"); + // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack + // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing + m.libraries = Json::ensureString(minecraft, QString("libraries"), QString()); + auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); + for (QJsonValueRef item : arr) + { + auto obj = Json::requireObject(item); + Flame::Modloader loader; + loadModloaderV1(loader, obj); + m.modLoaders.append(loader); + } + m.recommendedRAM = Json::ensureInteger(minecraft, "recommendedRam", 0); +} + +static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) +{ + auto mc = Json::requireObject(manifest, "minecraft"); + + loadMinecraftV1(pack.minecraft, mc); + + pack.name = Json::ensureString(manifest, QString("name"), "Unnamed"); + pack.version = Json::ensureString(manifest, QString("version"), QString()); + pack.author = Json::ensureString(manifest, QString("author"), "Anonymous"); + + auto arr = Json::ensureArray(manifest, "files", QJsonArray()); + for (auto item : arr) + { + auto obj = Json::requireObject(item); + + Flame::File file; + loadFileV1(file, obj); + Q_ASSERT(file.projectId != 0); + pack.files.insert(file.fileId, file); + } + + pack.overrides = Json::ensureString(manifest, "overrides", "overrides"); + + pack.is_loaded = true; +} + +void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) +{ + auto doc = Json::requireDocument(filepath); + auto obj = Json::requireObject(doc); + m.manifestType = Json::requireString(obj, "manifestType"); + if (m.manifestType != "minecraftModpack") + { + throw JSONValidationError("Not a modpack manifest!"); + } + m.manifestVersion = Json::requireInteger(obj, "manifestVersion"); + if (m.manifestVersion != 1) + { + throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion)); + } + loadManifestV1(m, obj); +} diff --git a/archived/projt-launcher/launcher/modplatform/flame/PackManifest.h b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.h new file mode 100644 index 0000000000..26856f5f60 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/flame/PackManifest.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include +#include +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceType.h" + +namespace Flame +{ + struct File + { + int projectId = 0; + int fileId = 0; + // NOTE: the opposite to 'optional' + bool required = true; + + ModPlatform::IndexedPack pack; + ModPlatform::IndexedVersion version; + + // our + QString targetFolder = QStringLiteral("mods"); + ModPlatform::ResourceType resourceType; + }; + + struct Modloader + { + QString id; + bool primary = false; + }; + + struct Minecraft + { + QString version; + QString libraries; + QList modLoaders; + int recommendedRAM; + }; + + struct Manifest + { + QString manifestType; + int manifestVersion = 0; + Flame::Minecraft minecraft; + QString name; + QString version; + QString author; + // File id -> File + QMap files; + QString overrides; + + bool is_loaded = false; + }; + + void loadManifest(Flame::Manifest& m, const QString& filepath); +} // namespace Flame diff --git a/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp new file mode 100644 index 0000000000..28e3b60c33 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.cpp @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ +#include "ExportToModList.h" +#include +#include +#include + +namespace ExportToModList +{ + QString toHTML(QList mods, OptionalData extraData) + { + QStringList lines; + for (auto mod : mods) + { + auto meta = mod->metadata(); + auto modName = mod->name().toHtmlEscaped(); + if (extraData & Url) + { + auto url = mod->homepage().toHtmlEscaped(); + if (!url.isEmpty()) + modName = QString("%2").arg(url, modName); + } + auto line = modName; + if (extraData & Version) + { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + if (!ver.isEmpty()) + line += QString(" [%1]").arg(ver.toHtmlEscaped()); + } + if (extraData & Authors && !mod->authors().isEmpty()) + line += " by " + mod->authors().join(", ").toHtmlEscaped(); + if (extraData & FileName) + line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped()); + + lines.append(QString("
  • %1
  • ").arg(line)); + } + return QString("
      \n\t%1\n
    ").arg(lines.join("\n\t")); + } + + QString toMarkdownEscaped(QString src) + { + for (auto ch : "\\`*_{}[]<>()#+-.!|") + src.replace(ch, QString("\\%1").arg(ch)); + return src; + } + + QString toMarkdown(QList mods, OptionalData extraData) + { + QStringList lines; + + for (auto mod : mods) + { + auto meta = mod->metadata(); + auto modName = toMarkdownEscaped(mod->name()); + if (extraData & Url) + { + auto url = mod->homepage(); + if (!url.isEmpty()) + modName = QString("[%1](%2)").arg(modName, url); + } + auto line = modName; + if (extraData & Version) + { + auto ver = toMarkdownEscaped(mod->version()); + if (ver.isEmpty() && meta != nullptr) + ver = toMarkdownEscaped(meta->version().toString()); + if (!ver.isEmpty()) + line += QString(" [%1]").arg(ver); + } + if (extraData & Authors && !mod->authors().isEmpty()) + line += " by " + toMarkdownEscaped(mod->authors().join(", ")); + if (extraData & FileName) + line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName())); + lines << "- " + line; + } + return lines.join("\n"); + } + + QString toPlainTXT(QList mods, OptionalData extraData) + { + QStringList lines; + for (auto mod : mods) + { + auto meta = mod->metadata(); + auto modName = mod->name(); + + auto line = modName; + if (extraData & Url) + { + auto url = mod->homepage(); + if (!url.isEmpty()) + line += QString(" (%1)").arg(url); + } + if (extraData & Version) + { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + if (!ver.isEmpty()) + line += QString(" [%1]").arg(ver); + } + if (extraData & Authors && !mod->authors().isEmpty()) + line += " by " + mod->authors().join(", "); + if (extraData & FileName) + line += QString(" (%1)").arg(mod->fileinfo().fileName()); + lines << line; + } + return lines.join("\n"); + } + + QString toJSON(QList mods, OptionalData extraData) + { + QJsonArray lines; + for (auto mod : mods) + { + auto meta = mod->metadata(); + auto modName = mod->name(); + QJsonObject line; + line["name"] = modName; + if (extraData & Url) + { + auto url = mod->homepage(); + if (!url.isEmpty()) + line["url"] = url; + } + if (extraData & Version) + { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + if (!ver.isEmpty()) + line["version"] = ver; + } + if (extraData & Authors && !mod->authors().isEmpty()) + line["authors"] = QJsonArray::fromStringList(mod->authors()); + if (extraData & FileName) + line["filename"] = mod->fileinfo().fileName(); + lines << line; + } + QJsonDocument doc; + doc.setArray(lines); + return doc.toJson(); + } + + QString toCSV(QList mods, OptionalData extraData) + { + QStringList lines; + for (auto mod : mods) + { + QStringList data; + auto meta = mod->metadata(); + auto modName = mod->name(); + + data << modName; + if (extraData & Url) + data << mod->homepage(); + if (extraData & Version) + { + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + data << ver; + } + if (extraData & Authors) + { + QString authors; + if (mod->authors().length() == 1) + authors = mod->authors().back(); + else if (mod->authors().length() > 1) + authors = QString("\"%1\"").arg(mod->authors().join(",")); + data << authors; + } + if (extraData & FileName) + data << mod->fileinfo().fileName(); + lines << data.join(","); + } + return lines.join("\n"); + } + + QString exportToModList(QList mods, Formats format, OptionalData extraData) + { + switch (format) + { + case HTML: return toHTML(mods, extraData); + case MARKDOWN: return toMarkdown(mods, extraData); + case PLAINTXT: return toPlainTXT(mods, extraData); + case JSON: return toJSON(mods, extraData); + case CSV: return toCSV(mods, extraData); + default: + { + return QString("unknown format:%1").arg(format); + } + } + } + + QString exportToModList(QList mods, QString lineTemplate) + { + QStringList lines; + for (auto mod : mods) + { + auto meta = mod->metadata(); + auto modName = mod->name(); + auto modID = mod->mod_id(); + auto url = mod->homepage(); + auto ver = mod->version(); + if (ver.isEmpty() && meta != nullptr) + ver = meta->version().toString(); + auto authors = mod->authors().join(", "); + auto filename = mod->fileinfo().fileName(); + lines << QString(lineTemplate) + .replace("{name}", modName) + .replace("{mod_id}", modID) + .replace("{url}", url) + .replace("{version}", ver) + .replace("{authors}", authors) + .replace("{filename}", filename); + } + return lines.join("\n"); + } +} // namespace ExportToModList \ No newline at end of file diff --git a/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h new file mode 100644 index 0000000000..5a1dd32826 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/helpers/ExportToModList.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ +#pragma once +#include +#include +#include "minecraft/mod/Mod.hpp" + +namespace ExportToModList +{ + + enum Formats + { + HTML, + MARKDOWN, + PLAINTXT, + JSON, + CSV, + CUSTOM + }; + enum OptionalData + { + Authors = 1 << 0, + Url = 1 << 1, + Version = 1 << 2, + FileName = 1 << 3 + }; + QString exportToModList(QList mods, Formats format, OptionalData extraData); + QString exportToModList(QList mods, QString lineTemplate); +} // namespace ExportToModList diff --git a/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp new file mode 100644 index 0000000000..2a36cc7d0d --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.cpp @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "HashUtils.h" + +#include +#include +#include +#include + +#include + +namespace Hashing +{ + + Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider) + { + switch (provider) + { + case ModPlatform::ResourceProvider::MODRINTH: + return makeShared( + file_path, + ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()); + case ModPlatform::ResourceProvider::FLAME: return makeShared(file_path, Algorithm::Murmur2); + default: + qCritical() << "[Hashing]" + << "Unrecognized mod platform!"; + return nullptr; + } + } + + Hasher::Ptr createHasher(QString file_path, QString type) + { + return makeShared(file_path, type); + } + + class QIODeviceReader : public Murmur2::Reader + { + public: + QIODeviceReader(QIODevice* device) : m_device(device) + {} + virtual ~QIODeviceReader() = default; + virtual int read(char* s, int n) + { + return m_device->read(s, n); + } + virtual bool eof() + { + return m_device->atEnd(); + } + virtual void goToBeginning() + { + m_device->seek(0); + } + virtual void close() + { + m_device->close(); + } + + private: + QIODevice* m_device; + }; + + QString algorithmToString(Algorithm type) + { + switch (type) + { + case Algorithm::Md4: return "md4"; + case Algorithm::Md5: return "md5"; + case Algorithm::Sha1: return "sha1"; + case Algorithm::Sha256: return "sha256"; + case Algorithm::Sha512: return "sha512"; + case Algorithm::Murmur2: return "murmur2"; + // case Algorithm::Unknown: + default: break; + } + return "unknown"; + } + + Algorithm algorithmFromString(QString type) + { + if (type == "md4") + return Algorithm::Md4; + if (type == "md5") + return Algorithm::Md5; + if (type == "sha1") + return Algorithm::Sha1; + if (type == "sha256") + return Algorithm::Sha256; + if (type == "sha512") + return Algorithm::Sha512; + if (type == "murmur2") + return Algorithm::Murmur2; + return Algorithm::Unknown; + } + + QString hash(QIODevice* device, Algorithm type) + { + if (!device->isOpen() && !device->open(QFile::ReadOnly)) + return ""; + QCryptographicHash::Algorithm alg = QCryptographicHash::Sha1; + switch (type) + { + case Algorithm::Md4: alg = QCryptographicHash::Algorithm::Md4; break; + case Algorithm::Md5: alg = QCryptographicHash::Algorithm::Md5; break; + case Algorithm::Sha1: alg = QCryptographicHash::Algorithm::Sha1; break; + case Algorithm::Sha256: alg = QCryptographicHash::Algorithm::Sha256; break; + case Algorithm::Sha512: alg = QCryptographicHash::Algorithm::Sha512; break; + case Algorithm::Murmur2: + { // CF-specific + auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; + auto reader = std::make_unique(device); + auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out)); + device->close(); + return result; + } + case Algorithm::Unknown: device->close(); return ""; + } + + QCryptographicHash hash(alg); + if (!hash.addData(device)) + qCritical() << "Failed to read JAR to create hash!"; + + Q_ASSERT(hash.result().length() == hash.hashLength(alg)); + auto result = hash.result().toHex(); + device->close(); + return result; + } + + QString hash(QString fileName, Algorithm type) + { + QFile file(fileName); + return hash(&file, type); + } + + QString hash(QByteArray data, Algorithm type) + { + QBuffer buff(&data); + return hash(&buff, type); + } + + void Hasher::executeTask() + { + m_future = QtConcurrent::run( + QThreadPool::globalInstance(), + [](QString fileName, Algorithm type) { return hash(fileName, type); }, + m_path, + m_alg); + connect(&m_watcher, + &QFutureWatcher::finished, + this, + [this] + { + if (m_future.isCanceled()) + { + emitAborted(); + } + else if (m_result = m_future.result(); m_result.isEmpty()) + { + emitFailed("Empty hash!"); + } + else + { + emitSucceeded(); + emit resultsReady(m_result); + } + }); + m_watcher.setFuture(m_future); + } + + bool Hasher::abort() + { + if (m_future.isRunning()) + { + m_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually + // cancels, which may not occur immediately. + return true; + } + return false; + } +} // namespace Hashing diff --git a/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h new file mode 100644 index 0000000000..571fbc4d1a --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/helpers/HashUtils.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include + +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +namespace Hashing +{ + + enum class Algorithm + { + Md4, + Md5, + Sha1, + Sha256, + Sha512, + Murmur2, + Unknown + }; + + QString algorithmToString(Algorithm type); + Algorithm algorithmFromString(QString type); + QString hash(QIODevice* device, Algorithm type); + QString hash(QString fileName, Algorithm type); + QString hash(QByteArray data, Algorithm type); + + class Hasher : public Task + { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + + Hasher(QString file_path, Algorithm alg) : m_path(file_path), m_alg(alg) + {} + Hasher(QString file_path, QString alg) : Hasher(file_path, algorithmFromString(alg)) + {} + + bool abort() override; + + void executeTask() override; + + QString getResult() const + { + return m_result; + }; + QString getPath() const + { + return m_path; + }; + + signals: + void resultsReady(QString hash); + + private: + QString m_result; + QString m_path; + Algorithm m_alg; + + QFuture m_future; + QFutureWatcher m_watcher; + }; + + Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider); + Hasher::Ptr createHasher(QString file_path, QString type); + +} // namespace Hashing diff --git a/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp new file mode 100644 index 0000000000..3a833cecd3 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "OverrideUtils.h" + +#include + +#include "FileSystem.h" + +namespace Override +{ + + void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path) + { + QString file_path(FS::PathCombine(parent_folder, name + ".txt")); + if (QFile::exists(file_path)) + FS::deletePath(file_path); + + FS::ensureFilePathExists(file_path); + + QFile file(file_path); + if (!file.open(QFile::WriteOnly)) + { + qWarning() << "Failed to open file '" << file.fileName() << "' for writing!"; + return; + } + + QDirIterator override_iterator(override_path, QDirIterator::Subdirectories); + while (override_iterator.hasNext()) + { + auto override_file_path = override_iterator.next(); + QFileInfo info(override_file_path); + if (info.isFile()) + { + // Absolute path with temp directory -> relative path + override_file_path = override_file_path.split(name).last().remove(0, 1); + + file.write(override_file_path.toUtf8()); + file.write("\n"); + } + } + + file.close(); + } + + QStringList readOverrides(const QString& name, const QString& parent_folder) + { + QString file_path(FS::PathCombine(parent_folder, name + ".txt")); + + QFile file(file_path); + if (!file.exists()) + return {}; + + QStringList previous_overrides; + + if (!file.open(QFile::ReadOnly)) + { + qWarning() << "Failed to open file '" << file.fileName() << "' for reading!"; + return previous_overrides; + } + + QString entry; + do + { + entry = file.readLine(); + previous_overrides.append(entry.trimmed()); + } + while (!entry.isEmpty()); + + file.close(); + + return previous_overrides; + } + +} // namespace Override diff --git a/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h new file mode 100644 index 0000000000..555a42d181 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/helpers/OverrideUtils.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +namespace Override +{ + + /** This creates a file in `parent_folder` that holds information about which + * overrides are in `override_path`. + * + * If there's already an existing such file, it will be ovewritten. + */ + void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path); + + /** This reads an existing overrides archive, returning a list of overrides. + * + * If there's no such file in `parent_folder`, it will return an empty list. + */ + QStringList readOverrides(const QString& name, const QString& parent_folder); + +} // namespace Override diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp new file mode 100644 index 0000000000..42726e4a81 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "modplatform/import_ftb/PackHelpers.h" + +#include +#include +#include +#include + +#include "FileSystem.h" +#include "Json.h" + +namespace FTBImportAPP +{ + + QIcon loadFTBIcon(const QString& imagePath) + { + // Map of type byte to image type string + static const QHash imageTypeMap = { { 0x00, "png" }, + { 0x01, "jpg" }, + { 0x02, "gif" }, + { 0x03, "webp" } }; + QFile file(imagePath); + if (!file.exists() || !file.open(QIODevice::ReadOnly)) + { + return QIcon(); + } + char type; + if (!file.getChar(&type)) + { + qDebug() << "Missing FTB image type header at" << imagePath; + return QIcon(); + } + if (!imageTypeMap.contains(type)) + { + qDebug().nospace().noquote() << "Don't recognize FTB image type 0x" << QString::number(type, 16); + return QIcon(); + } + + auto imageType = imageTypeMap[type]; + // Extract actual image data beyond the first byte + QImageReader reader(&file, imageType); + auto pixmap = QPixmap::fromImageReader(&reader); + if (pixmap.isNull()) + { + qDebug() << "The FTB image at" << imagePath << "is not valid"; + return QIcon(); + } + return QIcon(pixmap); + } + + Modpack parseDirectory(QString path) + { + Modpack modpack{ path }; + auto instanceFile = QFileInfo(FS::PathCombine(path, "instance.json")); + if (!instanceFile.exists() || !instanceFile.isFile()) + return {}; + try + { + auto doc = Json::requireDocument(instanceFile.absoluteFilePath(), "FTB_APP instance JSON file"); + const auto root = doc.object(); + modpack.uuid = Json::requireString(root, "uuid", "uuid"); + modpack.id = Json::requireInteger(root, "id", "id"); + modpack.versionId = Json::requireInteger(root, "versionId", "versionId"); + modpack.name = Json::requireString(root, "name", "name"); + modpack.version = Json::requireString(root, "version", "version"); + modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion"); + modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs"); + modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime"); + } + catch (const Exception& e) + { + qDebug() << "Couldn't load ftb instance json: " << e.cause(); + return {}; + } + + auto versionsFile = QFileInfo(FS::PathCombine(path, ".ftbapp", "version.json")); + if (!versionsFile.exists() || !versionsFile.isFile()) + { + versionsFile = QFileInfo(FS::PathCombine(path, "version.json")); + } + if (!versionsFile.exists() || !versionsFile.isFile()) + { + return {}; + } + try + { + auto doc = Json::requireDocument(versionsFile.absoluteFilePath(), "FTB_APP version JSON file"); + const auto root = doc.object(); + auto targets = Json::requireArray(root, "targets", "targets"); + + for (auto target : targets) + { + auto obj = Json::requireObject(target, "target"); + auto name = Json::requireString(obj, "name", "name"); + auto version = Json::requireString(obj, "version", "version"); + if (name == "neoforge") + { + modpack.loaderType = ModPlatform::NeoForge; + modpack.version = version; + break; + } + else if (name == "forge") + { + modpack.loaderType = ModPlatform::Forge; + modpack.version = version; + break; + } + else if (name == "fabric") + { + modpack.loaderType = ModPlatform::Fabric; + modpack.version = version; + break; + } + else if (name == "quilt") + { + modpack.loaderType = ModPlatform::Quilt; + modpack.version = version; + break; + } + } + } + catch (const Exception& e) + { + qDebug() << "Couldn't load ftb version json: " << e.cause(); + return {}; + } + auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg")); + if (iconFile.exists() && iconFile.isFile()) + { + modpack.icon = QIcon(iconFile.absoluteFilePath()); + } + else + { // the logo is a file that the first bit denotes the image tipe followed by the actual image data + modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo")); + } + return modpack; + } + +} // namespace FTBImportAPP diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h new file mode 100644 index 0000000000..47d7f83b22 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackHelpers.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ +#pragma once + +#include +#include +#include +#include +#include +#include "modplatform/ResourceAPI.h" + +namespace FTBImportAPP +{ + + struct Modpack + { + QString path; + + // json data + QString uuid; + int id; + int versionId; + QString name; + QString version; + QString mcVersion; + int totalPlayTime; + // not needed for instance creation + QVariant jvmArgs; + + std::optional loaderType; + QString loaderVersion; + + QIcon icon; + }; + + using ModpackList = QList; + + Modpack parseDirectory(QString path); + +} // namespace FTBImportAPP + +// We need it for the proxy model +Q_DECLARE_METATYPE(FTBImportAPP::Modpack) diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp new file mode 100644 index 0000000000..42e60a4ebb --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "PackInstallTask.h" + +#include + +#include "BaseInstance.h" +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/import_ftb/PackHelpers.h" +#include "settings/INISettingsObject.h" + +namespace FTBImportAPP +{ + + void PackInstallTask::executeTask() + { + setStatus(tr("Copying files...")); + setAbortable(false); + progress(1, 2); + + m_copyFuture = + QtConcurrent::run(QThreadPool::globalInstance(), + [this] + { + FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, "minecraft")); + folderCopy.followSymlinks(true); + return folderCopy(); + }); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::copySettings); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); + } + + void PackInstallTask::copySettings() + { + setStatus(tr("Copying settings...")); + progress(2, 2); + QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + instance.settings()->set("InstanceType", "OneSix"); + instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000); + + if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) + { + instance.settings()->set("OverrideJavaArgs", true); + instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString()); + } + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); + + auto modloader = m_pack.loaderType; + if (modloader.has_value()) + switch (modloader.value()) + { + case ModPlatform::NeoForge: + { + components->setComponentVersion("net.neoforged", m_pack.version, true); + break; + } + case ModPlatform::Forge: + { + components->setComponentVersion("net.minecraftforge", m_pack.version, true); + break; + } + case ModPlatform::Fabric: + { + components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.version, true); + break; + } + case ModPlatform::Quilt: + { + components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.version, true); + break; + } + case ModPlatform::Cauldron: break; + case ModPlatform::LiteLoader: break; + case ModPlatform::DataPack: break; + case ModPlatform::Babric: break; + case ModPlatform::BTA: break; + case ModPlatform::LegacyFabric: break; + case ModPlatform::Ornithe: break; + case ModPlatform::Rift: break; + case ModPlatform::Risugami: break; + case ModPlatform::StationLoader: break; + case ModPlatform::ModLoaderMP: break; + case ModPlatform::Optifine: break; + } + components->saveNow(); + + instance.setName(name()); + if (m_instIcon == "default") + m_instIcon = "ftb_logo"; + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + + emitSucceeded(); + } + +} // namespace FTBImportAPP diff --git a/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h new file mode 100644 index 0000000000..f58b0e7a3c --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/import_ftb/PackInstallTask.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +#include "InstanceTask.h" +#include "PackHelpers.h" + +namespace FTBImportAPP +{ + + class PackInstallTask : public InstanceTask + { + Q_OBJECT + + public: + explicit PackInstallTask(const Modpack& pack) : m_pack(pack) + {} + virtual ~PackInstallTask() = default; + + protected: + virtual void executeTask() override; + + private slots: + void copySettings(); + + private: + QFuture m_copyFuture; + QFutureWatcher m_copyFutureWatcher; + + const Modpack m_pack; + }; + +} // namespace FTBImportAPP diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp new file mode 100644 index 0000000000..5077a10d3b --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "PackFetchTask.h" +#include "PrivatePackManager.h" + +#include +#include "Application.h" +#include "BuildConfig.h" + +#include "net/ApiDownload.h" + +namespace LegacyFTB +{ + + void PackFetchTask::fetch() + { + publicPacks.clear(); + thirdPartyPacks.clear(); + + jobPtr.reset(new NetJob("LegacyFTB::ModpackFetch", m_network)); + + QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); + qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); + jobPtr->addNetAction(Net::ApiDownload::makeByteArray(publicPacksUrl, publicModpacksXmlFileData)); + + QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); + qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); + jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData)); + + connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); + connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); + connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); + + jobPtr->start(); + } + + void PackFetchTask::fetchPrivate(const QStringList& toFetch) + { + QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; + + for (auto& packCode : toFetch) + { + auto data = std::make_shared(); + NetJob* job = new NetJob("Fetching private pack", m_network); + job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data)); + job->setAskRetry(false); + + connect(job, + &NetJob::succeeded, + this, + [this, job, data, packCode] + { + ModpackList packs; + parseAndAddPacks(*data, PackType::Private, packs); + for (auto& currentPack : packs) + { + currentPack.packCode = packCode; + emit privateFileDownloadFinished(currentPack); + } + + job->deleteLater(); + + data->clear(); + }); + + connect(job, + &NetJob::failed, + this, + [this, job, packCode, data](QString reason) + { + emit privateFileDownloadFailed(reason, packCode); + job->deleteLater(); + + data->clear(); + }); + + connect(job, + &NetJob::aborted, + this, + [this, job, data] + { + emit aborted(); + job->deleteLater(); + + data->clear(); + }); + + job->start(); + } + } + + void PackFetchTask::fileDownloadFinished() + { + jobPtr.reset(); + + QStringList failedLists; + + if (!parseAndAddPacks(*publicModpacksXmlFileData, PackType::Public, publicPacks)) + { + failedLists.append(tr("Public Packs")); + } + + if (!parseAndAddPacks(*thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) + { + failedLists.append(tr("Third Party Packs")); + } + + if (failedLists.size() > 0) + { + emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- "))); + } + else + { + emit finished(publicPacks, thirdPartyPacks); + } + } + + bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list) + { + QDomDocument doc; + + QString errorMsg = "Unknown error."; + int errorLine = -1; + int errorCol = -1; + + if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) + { + auto fullErrMsg = + QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol); + qWarning() << fullErrMsg; + data.clear(); + return false; + } + + QDomNodeList nodes = doc.elementsByTagName("modpack"); + for (int i = 0; i < nodes.length(); i++) + { + QDomElement element = nodes.at(i).toElement(); + + Modpack modpack; + modpack.name = element.attribute("name"); + modpack.currentVersion = element.attribute("version"); + modpack.mcVersion = element.attribute("mcVersion"); + modpack.description = element.attribute("description"); + modpack.mods = element.attribute("mods"); + modpack.logo = element.attribute("logo"); + modpack.oldVersions = element.attribute("oldVersions").split(";"); + modpack.broken = false; + modpack.bugged = false; + + // remove empty if the xml is bugged + for (QString curr : modpack.oldVersions) + { + if (curr.isNull() || curr.isEmpty()) + { + modpack.oldVersions.removeAll(curr); + modpack.bugged = true; + qWarning() << "Removed some empty versions from" << modpack.name; + } + } + + if (modpack.oldVersions.size() < 1) + { + if (!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) + { + modpack.oldVersions.append(modpack.currentVersion); + qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + + ")"; + } + else + { + modpack.broken = true; + qWarning() << "Broken pack:" << modpack.name << " => No valid version!"; + } + } + + modpack.author = element.attribute("author"); + + modpack.dir = element.attribute("dir"); + modpack.file = element.attribute("url"); + + modpack.type = packType; + + list.append(modpack); + } + + return true; + } + + void PackFetchTask::fileDownloadFailed(QString reason) + { + qWarning() << "Fetching FTBPacks failed:" << reason; + emit failed(reason); + } + + void PackFetchTask::fileDownloadAborted() + { + emit aborted(); + } + +} // namespace LegacyFTB diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h new file mode 100644 index 0000000000..1c40c6cc88 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include +#include "PackHelpers.h" +#include "net/NetJob.h" + +namespace LegacyFTB +{ + + class PackFetchTask : public QObject + { + Q_OBJECT + + public: + PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; + virtual ~PackFetchTask() = default; + + void fetch(); + void fetchPrivate(const QStringList& toFetch); + + private: + shared_qobject_ptr m_network; + NetJob::Ptr jobPtr; + + std::shared_ptr publicModpacksXmlFileData = std::make_shared(); + std::shared_ptr thirdPartyModpacksXmlFileData = std::make_shared(); + + bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list); + ModpackList publicPacks; + ModpackList thirdPartyPacks; + + protected slots: + void fileDownloadFinished(); + void fileDownloadFailed(QString reason); + void fileDownloadAborted(); + + signals: + void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); + void failed(QString reason); + void aborted(); + + void privateFileDownloadFinished(const Modpack& modpack); + void privateFileDownloadFailed(QString reason, QString packCode); + }; + +} // namespace LegacyFTB diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h new file mode 100644 index 0000000000..ac7cf75da3 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackHelpers.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include +#include + +namespace LegacyFTB +{ + + // Header for structs etc... + enum class PackType + { + Public, + ThirdParty, + Private + }; + + struct Modpack + { + QString name; + QString description; + QString author; + QStringList oldVersions; + QString currentVersion; + QString mcVersion; + QString mods; + QString logo; + + // Technical data + QString dir; + QString file; //<- Url in the xml, but doesn't make much sense + + bool bugged = false; + bool broken = false; + + PackType type; + QString packCode; + }; + + using ModpackList = QList; + +} // namespace LegacyFTB + +// We need it for the proxy model +Q_DECLARE_METATYPE(LegacyFTB::Modpack) diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp new file mode 100644 index 0000000000..ec8472506b --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "PackInstallTask.h" + +#include + +#include "BaseInstance.h" +#include "FileSystem.h" +#include "MMCZip.h" +#include "minecraft/GradleSpecifier.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" + +#include "Application.h" +#include "BuildConfig.h" + +#include "net/ApiDownload.h" + +namespace LegacyFTB +{ + + PackInstallTask::PackInstallTask(shared_qobject_ptr network, + const Modpack& pack, + QString version) + { + m_pack = pack; + m_version = version; + m_network = network; + } + + void PackInstallTask::executeTask() + { + downloadPack(); + } + + void PackInstallTask::downloadPack() + { + setStatus(tr("Downloading zip for %1").arg(m_pack.name)); + setProgress(1, 4); + setAbortable(false); + + auto path = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); + auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", path); + entry->setStale(true); + archivePath = entry->getFullPath(); + netJobContainer.reset(new NetJob("Download FTB Pack", m_network)); + QString url; + if (m_pack.type == PackType::Private) + { + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(path); + } + else + { + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(path); + } + netJobContainer->addNetAction(Net::ApiDownload::makeCached(url, entry)); + + connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip); + connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed); + connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); + connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted); + + netJobContainer->start(); + + setAbortable(true); + progress(1, 4); + } + + void PackInstallTask::unzip() + { + setStatus(tr("Extracting modpack")); + setAbortable(false); + progress(2, 4); + + QDir extractDir(m_stagingPath); + + m_packZip.reset(new QuaZip(archivePath)); + if (!m_packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Failed to open modpack file %1!").arg(archivePath)); + return; + } + + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), + QOverload::of(MMCZip::extractDir), + archivePath, + extractDir.absolutePath() + "/unzip"); + connect(&m_extractFutureWatcher, + &QFutureWatcher::finished, + this, + &PackInstallTask::onUnzipFinished); + connect(&m_extractFutureWatcher, + &QFutureWatcher::canceled, + this, + &PackInstallTask::onUnzipCanceled); + m_extractFutureWatcher.setFuture(m_extractFuture); + } + + void PackInstallTask::onUnzipFinished() + { + install(); + } + + void PackInstallTask::onUnzipCanceled() + { + emitAborted(); + } + + void PackInstallTask::install() + { + setStatus(tr("Installing modpack")); + progress(3, 4); + QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); + if (unzipMcDir.exists()) + { + // ok, found minecraft dir, move contents to instance dir + if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) + { + emitFailed(tr("Failed to move unpacked Minecraft!")); + return; + } + } + + QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); + + bool fallback = true; + + // handle different versions + QFile packJson(m_stagingPath + "/minecraft/pack.json"); + QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); + if (packJson.exists()) + { + if (packJson.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); + packJson.close(); + + // we only care about the libs + QJsonArray libs = doc.object().value("libraries").toArray(); + + for (const auto& value : libs) + { + QString nameValue = value.toObject().value("name").toString(); + if (!nameValue.startsWith("net.minecraftforge")) + { + continue; + } + + GradleSpecifier forgeVersion(nameValue); + + components->setComponentVersion( + "net.minecraftforge", + forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", "")); + packJson.remove(); + fallback = false; + break; + } + } + else + { + qWarning() << "Failed to open file '" << packJson.fileName() << "' for reading!"; + } + } + + if (jarmodDir.exists()) + { + qDebug() << "Found jarmods, installing..."; + + QStringList jarmods; + for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) + { + qDebug() << "Jarmod:" << info.fileName(); + jarmods.push_back(info.absoluteFilePath()); + } + + components->installJarMods(jarmods); + fallback = false; + } + + // just nuke unzip directory, it s not needed anymore + FS::deletePath(m_stagingPath + "/unzip"); + + if (fallback) + { + // Fallback to vanilla Minecraft if no modloader was detected + qWarning() << "No Forge version or jarmods found, creating vanilla instance"; + // Components already has Minecraft version set, no need to fail + // Just continue with vanilla Minecraft + fallback = false; + } + + components->saveNow(); + + progress(4, 4); + + instance.setName(name()); + if (m_instIcon == "default") + { + m_instIcon = "ftb_logo"; + } + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + + emitSucceeded(); + } + + bool PackInstallTask::abort() + { + if (!canAbort()) + { + return false; + } + + netJobContainer->abort(); + return InstanceTask::abort(); + } + +} // namespace LegacyFTB diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h new file mode 100644 index 0000000000..07575fa483 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once +#include +#include +#include "InstanceTask.h" +#include "PackHelpers.h" +#include "meta/Index.hpp" +#include "meta/Version.hpp" +#include "meta/VersionList.hpp" +#include "net/NetJob.h" + +#include "net/NetJob.h" + +#include + +namespace LegacyFTB +{ + + class PackInstallTask : public InstanceTask + { + Q_OBJECT + + public: + explicit PackInstallTask(shared_qobject_ptr network, + const Modpack& pack, + QString version); + virtual ~PackInstallTask() + {} + + bool canAbort() const override + { + return true; + } + bool abort() override; + + protected: + //! Entry point for tasks. + virtual void executeTask() override; + + private: + void downloadPack(); + void unzip(); + void install(); + + private slots: + + void onUnzipFinished(); + void onUnzipCanceled(); + + private: /* data */ + shared_qobject_ptr m_network; + bool abortable = false; + std::unique_ptr m_packZip; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + NetJob::Ptr netJobContainer; + QString archivePath; + + Modpack m_pack; + QString m_version; + }; + +} // namespace LegacyFTB diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp new file mode 100644 index 0000000000..80d5906e84 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "PrivatePackManager.h" + +#include + +#include "FileSystem.h" + +namespace LegacyFTB +{ + + void PrivatePackManager::load() + { + try + { + auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts); + currentPacks = QSet(foo.begin(), foo.end()); + + dirty = false; + } + catch (...) + { + currentPacks = {}; + qWarning() << "Failed to read third party FTB pack codes from" << m_filename; + } + } + + void PrivatePackManager::save() const + { + if (!dirty) + { + return; + } + try + { + QStringList list = currentPacks.values(); + FS::write(m_filename, list.join('\n').toUtf8()); + dirty = false; + } + catch (...) + { + qWarning() << "Failed to write third party FTB pack codes to" << m_filename; + } + } + +} // namespace LegacyFTB diff --git a/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h new file mode 100644 index 0000000000..918d1e98bd --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/legacy_ftb/PrivatePackManager.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include +#include +#include + +namespace LegacyFTB +{ + + class PrivatePackManager + { + public: + ~PrivatePackManager() + { + save(); + } + void load(); + void save() const; + bool empty() const + { + return currentPacks.empty(); + } + const QSet& getCurrentPackCodes() const + { + return currentPacks; + } + void add(const QString& code) + { + currentPacks.insert(code); + dirty = true; + } + void remove(const QString& code) + { + currentPacks.remove(code); + dirty = true; + } + + private: + QSet currentPacks; + QString m_filename = "private_packs.txt"; + mutable bool dirty = false; + }; + +} // namespace LegacyFTB diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp new file mode 100644 index 0000000000..f19889a66c --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * ======================================================================== */ + +#include "ModrinthAPI.h" + +#include "Application.h" +#include "Json.h" +#include "net/ApiDownload.h" +#include "net/ApiUpload.h" +#include "net/NetJob.h" +#include "net/Upload.h" + +Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr response) +{ + auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); + + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), + response)); + + return netJob; +} + +Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, + QString hash_format, + std::shared_ptr response) +{ + auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction( + Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); + netJob->setAskRetry(false); + return netJob; +} + +Task::Ptr ModrinthAPI::latestVersion(QString hash, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response) +{ + auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); + + QJsonObject body_obj; + + if (loaders.has_value()) + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); + + if (mcVersions.has_value()) + { + QStringList game_versions; + for (auto& ver : mcVersions.value()) + { + game_versions.append(mapMCVersionToModrinth(ver)); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + } + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::ApiUpload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), + response, + body_raw)); + + return netJob; +} + +Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response) +{ + auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + if (loaders.has_value()) + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); + + if (mcVersions.has_value()) + { + QStringList game_versions; + for (auto& ver : mcVersions.value()) + { + game_versions.append(mapMCVersionToModrinth(ver)); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + } + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), + response, + body_raw)); + + return netJob; +} + +Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr response) const +{ + auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); + auto searchUrl = getMultipleModInfoURL(addonIds); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + + return netJob; +} + +QList ModrinthAPI::getSortingMethods() const +{ + // https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects + return { { 1, "relevance", QObject::tr("Sort by Relevance") }, + { 2, "downloads", QObject::tr("Sort by Downloads") }, + { 3, "follows", QObject::tr("Sort by Follows") }, + { 4, "newest", QObject::tr("Sort by Newest") }, + { 5, "updated", QObject::tr("Sort by Last Updated") } }; +} + +Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) +{ + auto netJob = makeShared(QString("Modrinth::GetCategories"), APPLICATION->network()); + netJob->addNetAction( + Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response)); + QObject::connect(netJob.get(), + &Task::failed, + [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; }); + return netJob; +} + +QList ModrinthAPI::loadCategories(std::shared_ptr response, QString projectType) +{ + QList categories; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from categories at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return categories; + } + + try + { + auto arr = Json::requireArray(doc); + + for (auto val : arr) + { + auto cat = Json::requireObject(val); + auto name = Json::requireString(cat, "name"); + if (Json::ensureString(cat, "project_type", "") == projectType) + categories.push_back({ name, name }); + } + } + catch (Json::JsonException& e) + { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + return categories; +} + +QList ModrinthAPI::loadModCategories(std::shared_ptr response) +{ + return loadCategories(response, "mod"); +}; diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h new file mode 100644 index 0000000000..e1637a7e8a --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthAPI.h @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * + * + * ======================================================================== */ + +#pragma once + +#include "BuildConfig.h" +#include "Json.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" + +#include + +class ModrinthAPI : public ResourceAPI +{ + public: + Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr response); + + Task::Ptr currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response); + + Task::Ptr latestVersion(QString hash, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response); + + Task::Ptr latestVersions(const QStringList& hashes, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response); + + Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + + static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadCategories(std::shared_ptr response, QString projectType); + static QList loadModCategories(std::shared_ptr response); + + public: + auto getSortingMethods() const -> QList override; + + inline auto getAuthorURL(const QString& name) const -> QString + { + return "https://modrinth.com/user/" + name; + }; + + static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList + { + QStringList l; + for (auto loader : { ModPlatform::NeoForge, + ModPlatform::Forge, + ModPlatform::Fabric, + ModPlatform::Quilt, + ModPlatform::LiteLoader, + ModPlatform::DataPack, + ModPlatform::Babric, + ModPlatform::BTA, + ModPlatform::LegacyFabric, + ModPlatform::Ornithe, + ModPlatform::Rift, + ModPlatform::Risugami, + ModPlatform::StationLoader, + ModPlatform::ModLoaderMP, + ModPlatform::Optifine }) + { + if (types & loader) + { + l << getModLoaderAsString(loader); + } + } + return l; + } + + static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString + { + QStringList l; + for (auto loader : getModLoaderStrings(types)) + { + l << QString("\"categories:%1\"").arg(loader); + } + return l.join(','); + } + + static auto getCategoriesFilters(QStringList categories) -> const QString + { + QStringList l; + for (auto cat : categories) + { + l << QString("\"categories:%1\"").arg(cat); + } + return l.join(','); + } + + static QString getSideFilters(ModPlatform::Side side) + { + switch (side) + { + case ModPlatform::Side::ClientSide: + return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_" + "side:unsupported\""); + case ModPlatform::Side::ServerSide: + return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_" + "side:unsupported\""); + case ModPlatform::Side::UniversalSide: + return QString("\"client_side:required\"],[\"server_side:required\""); + case ModPlatform::Side::NoSide: + // fallthrough + default: return {}; + } + } + + static inline QString mapMCVersionFromModrinth(QString v) + { + static const QString preString = " Pre-Release "; + bool pre = false; + if (v.contains("-pre")) + { + pre = true; + v.replace("-pre", preString); + } + v.replace("-", " "); + if (pre) + { + v.replace(" Pre Release ", preString); + } + return v; + } + + private: + static QString resourceTypeParameter(ModPlatform::ResourceType type) + { + switch (type) + { + case ModPlatform::ResourceType::Mod: return "mod"; + case ModPlatform::ResourceType::ResourcePack: return "resourcepack"; + case ModPlatform::ResourceType::ShaderPack: return "shader"; + case ModPlatform::ResourceType::DataPack: return "datapack"; + case ModPlatform::ResourceType::Modpack: return "modpack"; + default: qWarning() << "Invalid resource type for Modrinth API!"; break; + } + + return ""; + } + + QString createFacets(SearchArgs const& args) const + { + QStringList facets_list; + + if (args.loaders.has_value() && args.loaders.value() != 0) + facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); + if (args.versions.has_value() && !args.versions.value().empty()) + facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); + if (args.side.has_value()) + { + auto side = getSideFilters(args.side.value()); + if (!side.isEmpty()) + facets_list.append(QString("[%1]").arg(side)); + } + if (args.categoryIds.has_value() && !args.categoryIds->empty()) + facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value()))); + if (args.openSource) + facets_list.append("[\"open_source:true\"]"); + + facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); + + return QString("[%1]").arg(facets_list.join(',')); + } + + public: + inline auto getSearchURL(SearchArgs const& args) const -> std::optional override + { + if (args.loaders.has_value() && args.loaders.value() != 0) + { + if (!validateModLoaders(args.loaders.value())) + { + qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!"; + return {}; + } + } + + QStringList get_arguments; + get_arguments.append(QString("offset=%1").arg(args.offset)); + get_arguments.append(QString("limit=25")); + if (args.search.has_value()) + get_arguments.append(QString("query=%1").arg(args.search.value())); + if (args.sorting.has_value()) + get_arguments.append(QString("index=%1").arg(args.sorting.value().name)); + get_arguments.append(QString("facets=%1").arg(createFacets(args))); + + return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&'); + }; + + inline auto getInfoURL(QString const& id) const -> std::optional override + { + return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; + }; + + inline auto getMultipleModInfoURL(QStringList ids) const -> QString + { + return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); + }; + + inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional override + { + QStringList get_arguments; + if (args.mcVersions.has_value()) + get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value()))); + if (args.loaders.has_value()) + get_arguments.append( + QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); + + return QString("%1/project/%2/version%3%4") + .arg(BuildConfig.MODRINTH_PROD_URL, + args.pack->addonId.toString(), + get_arguments.isEmpty() ? "" : "?", + get_arguments.join('&')); + }; + + QString getGameVersionsArray(std::list mcVersions) const + { + QString s; + for (auto& ver : mcVersions) + { + s += QString("\"versions:%1\",").arg(mapMCVersionToModrinth(ver)); + } + s.remove(s.length() - 1, 1); // remove last comma + return s.isEmpty() ? QString() : s; + } + + static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool + { + return loaders + & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt + | ModPlatform::LiteLoader | ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA + | ModPlatform::LegacyFabric | ModPlatform::Ornithe | ModPlatform::Rift | ModPlatform::Risugami + | ModPlatform::StationLoader | ModPlatform::ModLoaderMP | ModPlatform::Optifine); + } + + std::optional getDependencyURL(DependencySearchArgs const& args) const override + { + return args.dependency.version.length() != 0 + ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) + : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]") + .arg(BuildConfig.MODRINTH_PROD_URL) + .arg(args.dependency.addonId.toString()) + .arg(mapMCVersionToModrinth(args.mcVersion)) + .arg(getModLoaderStrings(args.loader).join("\",\"")); + }; + + QJsonArray documentToArray(QJsonDocument& obj) const override + { + return obj.object().value("hits").toArray(); + } + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override + { + Modrinth::loadIndexedPack(m, obj); + } + ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const override + { + return Modrinth::loadIndexedPackVersion(obj); + }; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override + { + Modrinth::loadExtraPackData(m, obj); + } +}; diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp new file mode 100644 index 0000000000..fdac618a44 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ModrinthCheckUpdate.h" +#include "Application.h" +#include "ModrinthAPI.h" +#include "ModrinthPackIndex.h" + +#include "Json.h" + +#include "QObjectPtr.h" +#include "ResourceDownloadTask.h" + +#include "modplatform/ModIndex.h" +#include "modplatform/helpers/HashUtils.h" + +#include "tasks/ConcurrentTask.h" + +static ModrinthAPI api; + +ModrinthCheckUpdate::ModrinthCheckUpdate(QList& resources, + std::list& mcVersions, + QList loadersList, + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)), + m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) +{ + if (!m_loadersList.isEmpty()) + { // this is for mods so append all the other posible loaders to the initial list + m_initialSize = m_loadersList.length(); + ModPlatform::ModLoaderTypes modLoaders; + for (auto m : resources) + { + modLoaders |= m->metadata()->loaders; + } + for (auto l : m_loadersList) + { + modLoaders &= ~l; + } + m_loadersList.append(ModPlatform::modLoaderTypesToList(modLoaders)); + } +} + +bool ModrinthCheckUpdate::abort() +{ + if (m_job) + return m_job->abort(); + return true; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void ModrinthCheckUpdate::executeTask() +{ + setStatus(tr("Preparing resources for Modrinth...")); + setProgress(0, (m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2 + 1); + + auto hashing_task = makeShared("MakeModrinthHashesTask", + APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + bool startHasing = false; + for (auto* resource : m_resources) + { + auto hash = resource->metadata()->hash; + + // Sadly the API can only handle one hash type per call, se we + // need to generate a new hash if the current one is innadequate + // (though it will rarely happen, if at all) + if (resource->metadata()->hash_format != m_hashType) + { + auto hash_task = + Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); + connect(hash_task.get(), + &Hashing::Hasher::resultsReady, + [this, resource](QString hash) { m_mappings.insert(hash, resource); }); + connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); + hashing_task->addTask(hash_task); + startHasing = true; + } + else + { + m_mappings.insert(hash, resource); + } + } + + if (startHasing) + { + connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); + m_job = hashing_task; + hashing_task->start(); + } + else + { + checkNextLoader(); + } +} + +void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional loader, + bool forceModLoaderCheck) +{ + setStatus(tr("Waiting for the API response from Modrinth...")); + setProgress(m_progress + 1, m_progressTotal); + + auto response = std::make_shared(); + QStringList hashes; + if (forceModLoaderCheck && loader.has_value()) + { + for (auto hash : m_mappings.keys()) + { + if (m_mappings[hash]->metadata()->loaders & loader.value()) + { + hashes.append(hash); + } + } + } + else + { + hashes = m_mappings.keys(); + } + auto job = api.latestVersions(hashes, m_hashType, m_gameVersions, loader, response); + + connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); }); + + connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); + + m_job = job; + m_loaderIdx++; + job->start(); +} + +void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr response, + std::optional loader) +{ + setStatus(tr("Parsing the API response from Modrinth...")); + setProgress(m_progress + 1, m_progressTotal); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + emitFailed(parse_error.errorString()); + return; + } + + try + { + auto iter = m_mappings.begin(); + + while (iter != m_mappings.end()) + { + const QString hash = iter.key(); + Resource* resource = iter.value(); + + auto project_obj = doc[hash].toObject(); + + // If the returned project is empty, but we have Modrinth metadata, + // it means this specific version is not available + if (project_obj.isEmpty()) + { + qDebug() << "Mod " << m_mappings.find(hash).value()->name() << " got an empty response." + << "Hash: " << hash; + ++iter; + continue; + } + + // Sometimes a version may have multiple files, one with "forge" and one with "fabric", + // so we may want to filter it + QString loader_filter; + if (loader.has_value() && loader != 0) + { + auto modLoaders = ModPlatform::modLoaderTypesToList(*loader); + if (!modLoaders.isEmpty()) + { + loader_filter = ModPlatform::getModLoaderAsString(modLoaders.first()); + } + } + + // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: + // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of + // the loader_filter + // - The version reported by the JAR is different from the version reported by the indexed version (it's + // usually the case) Such is the pain of having arbitrary files for a given version .-. + + auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hashType, loader_filter); + if (project_ver.downloadUrl.isEmpty()) + { + qCritical() << "Modrinth mod without download url!" << project_ver.fileName; + ++iter; + continue; + } + + // Fake pack with the necessary info to pass to the download task :) + auto pack = std::make_shared(); + pack->name = resource->name(); + pack->slug = resource->metadata()->slug; + pack->addonId = resource->metadata()->project_id; + pack->provider = ModPlatform::ResourceProvider::MODRINTH; + if ((project_ver.hash != hash && project_ver.is_preferred) + || (resource->status() == ResourceStatus::NOT_INSTALLED)) + { + auto download_task = makeShared(pack, project_ver, m_resourceModel); + + QString old_version = resource->metadata()->version_number; + if (old_version.isEmpty()) + { + if (resource->status() == ResourceStatus::NOT_INSTALLED) + old_version = tr("Not installed"); + else + old_version = tr("Unknown"); + } + + m_updates.emplace_back(pack->name, + hash, + old_version, + project_ver.version_number, + project_ver.version_type, + project_ver.changelog, + ModPlatform::ResourceProvider::MODRINTH, + download_task, + resource->enabled()); + } + m_deps.append(std::make_shared(pack, project_ver)); + + iter = m_mappings.erase(iter); + } + } + catch (Json::JsonException& e) + { + emitFailed(e.cause() + ": " + e.what()); + return; + } + checkNextLoader(); +} + +void ModrinthCheckUpdate::checkNextLoader() +{ + if (m_mappings.isEmpty()) + { + emitSucceeded(); + return; + } + if (m_loaderIdx < m_loadersList.size()) + { // this are mods so check with loades + getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize); + return; + } + else if (m_loadersList.isEmpty() && m_loaderIdx == 0) + { // this are other resources no need to check more than once with empty loader + getUpdateModsForLoader(); + return; + } + + for (auto resource : m_mappings) + { + QString reason; + + if (dynamic_cast(resource) != nullptr) + reason = tr("No valid version found for this resource. It's probably unavailable for the current game " + "version / mod loader."); + else + reason = + tr("No valid version found for this resource. It's probably unavailable for the current game version."); + + emit checkFailed(resource, reason); + } + + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h new file mode 100644 index 0000000000..d6ab02d653 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include "modplatform/CheckUpdateTask.h" + +class ModrinthCheckUpdate : public CheckUpdateTask +{ + Q_OBJECT + + public: + ModrinthCheckUpdate(QList& resources, + std::list& mcVersions, + QList loadersList, + std::shared_ptr resourceModel); + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + void getUpdateModsForLoader(std::optional loader = {}, + bool forceModLoaderCheck = false); + void checkVersionsResponse(std::shared_ptr response, std::optional loader); + void checkNextLoader(); + + private: + Task::Ptr m_job = nullptr; + QHash m_mappings; + QString m_hashType; + int m_loaderIdx = 0; + int m_initialSize = 0; +}; diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp new file mode 100644 index 0000000000..23e5500fa1 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.cpp @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== + */ + +#include "ModrinthCollectionImportTask.h" + +#include "Application.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "net/ApiDownload.h" +#include "net/NetJob.h" + +#include +#include +#include +#include + +#include + +namespace +{ + constexpr int kProjectBatchSize = 50; +} + +ModrinthCollectionImportTask::ModrinthCollectionImportTask(QString collection_reference, MinecraftInstance* instance) + : Task(false), + m_collection_reference(std::move(collection_reference).trimmed()), + m_instance(instance) +{ + if (m_instance) + { + auto pack_profile = m_instance->getPackProfile(); + if (auto minecraft = pack_profile->getComponent("net.minecraft"); minecraft) + m_mc_versions.push_back(minecraft->getVersion()); + m_loaders = pack_profile->getSupportedModLoaders().value_or(ModPlatform::ModLoaderTypes{}); + } +} + +bool ModrinthCollectionImportTask::abort() +{ + if (!canAbort()) + return false; + + if (m_current_task) + return m_current_task->abort(); + + emitAborted(); + return true; +} + +void ModrinthCollectionImportTask::executeTask() +{ + setAbortable(true); + + if (!m_instance) + { + emitFailed(tr("Missing instance context for Modrinth collection import.")); + return; + } + + m_collection_reference = normalizeCollectionReference(m_collection_reference); + if (m_collection_reference.isEmpty()) + { + emitFailed(tr("Enter a valid Modrinth collection URL or collection ID.")); + return; + } + + fetchCollectionPage(); +} + +QString ModrinthCollectionImportTask::normalizeCollectionReference(QString reference) const +{ + reference = reference.trimmed(); + if (reference.isEmpty()) + return {}; + + static const QRegularExpression collectionPathPattern(R"(^(?:/?collection/)?([^/?#]+))"); + + if (reference.startsWith("http://") || reference.startsWith("https://")) + { + QUrl url(reference); + if (!url.isValid()) + return {}; + + const auto host = url.host(); + if (host != "modrinth.com" && host != "www.modrinth.com") + return {}; + + auto match = collectionPathPattern.match(url.path().mid(1)); + return match.hasMatch() ? match.captured(1) : QString{}; + } + + auto match = collectionPathPattern.match(reference); + return match.hasMatch() ? match.captured(1) : reference; +} + +QString ModrinthCollectionImportTask::collectionUrl() const +{ + return QString("https://modrinth.com/collection/%1") + .arg(QString::fromUtf8(QUrl::toPercentEncoding(m_collection_reference))); +} + +bool ModrinthCollectionImportTask::extractCollectionMetadata(const QByteArray& html) +{ + QString text = QString::fromUtf8(html); + + static const QRegularExpression titlePattern(R"(([^<]+))"); + if (auto match = titlePattern.match(text); match.hasMatch()) + { + m_collection_name = match.captured(1).trimmed(); + const QString suffix = QStringLiteral(" - Modrinth"); + if (m_collection_name.endsWith(suffix)) + m_collection_name.chop(suffix.size()); + } + + QStringList best_ids; + static const QRegularExpression projectsPattern(R"(projects\?ids=%5B([^"]+)%5D)"); + auto match_it = projectsPattern.globalMatch(text); + while (match_it.hasNext()) + { + auto match = match_it.next(); + QStringList ids; + auto decoded = QUrl::fromPercentEncoding(match.captured(1).toUtf8()); + + static const QRegularExpression idPattern(QStringLiteral("\"([A-Za-z0-9]+)\"")); + auto id_it = idPattern.globalMatch(decoded); + QSet seen; + while (id_it.hasNext()) + { + auto id_match = id_it.next(); + auto id = id_match.captured(1); + if (!seen.contains(id)) + { + seen.insert(id); + ids.append(id); + } + } + + if (ids.size() > best_ids.size()) + best_ids = ids; + } + + m_project_ids = best_ids; + return !m_project_ids.isEmpty(); +} + +void ModrinthCollectionImportTask::fetchCollectionPage() +{ + setStatus(tr("Loading Modrinth collection page...")); + setProgress(0, 1); + + auto response = std::make_shared(); + auto job = makeShared(tr("Modrinth collection page"), APPLICATION->network()); + job->addNetAction(Net::ApiDownload::makeByteArray(QUrl(collectionUrl()), response)); + + connect(job.get(), + &NetJob::succeeded, + this, + [this, response] + { + if (!extractCollectionMetadata(*response)) + { + emitFailed(tr("Could not extract any projects from this Modrinth collection page.")); + return; + } + + const int project_batches = (m_project_ids.size() + kProjectBatchSize - 1) / kProjectBatchSize; + setProgress(1, 1 + project_batches + m_project_ids.size()); + QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchProjectBatch, Qt::QueuedConnection); + }); + connect(job.get(), &NetJob::failed, this, &ModrinthCollectionImportTask::emitFailed); + connect(job.get(), &NetJob::aborted, this, &ModrinthCollectionImportTask::emitAborted); + connect(job.get(), &NetJob::stepProgress, this, &ModrinthCollectionImportTask::propagateStepProgress); + + m_current_task = job; + job->start(); +} + +void ModrinthCollectionImportTask::fetchProjectBatch() +{ + if (m_project_batch_offset >= m_project_ids.size()) + { + QHash pack_lookup; + for (auto& pack : std::as_const(m_packs)) + pack_lookup.insert(pack->addonId.toString(), pack); + + QList ordered_packs; + for (auto& id : std::as_const(m_project_ids)) + { + if (pack_lookup.contains(id)) + ordered_packs.append(pack_lookup.value(id)); + } + m_packs = ordered_packs; + + if (m_packs.isEmpty()) + { + emitFailed( + tr("This collection does not contain any projects that could be resolved through the Modrinth API.")); + return; + } + + QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection); + return; + } + + setStatus(tr("Loading collection projects...")); + + auto response = std::make_shared(); + const auto batch = m_project_ids.mid(m_project_batch_offset, kProjectBatchSize); + auto job = m_api.getProjects(batch, response); + if (!job) + { + emitFailed(tr("Failed to request Modrinth project metadata for this collection.")); + return; + } + + connect(job.get(), + &Task::succeeded, + this, + [this, response, batch] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + emitFailed(tr("Failed to parse Modrinth project metadata for this collection.")); + return; + } + + if (!doc.isArray()) + { + emitFailed(tr("Modrinth returned an unexpected project list for this collection.")); + return; + } + + QSet seen_in_batch; + for (auto entry : doc.array()) + { + try + { + auto obj = Json::requireObject(entry); + auto pack = std::make_shared(); + m_api.loadIndexedPack(*pack, obj); + m_api.loadExtraPackInfo(*pack, obj); + if (!seen_in_batch.contains(pack->addonId.toString())) + { + m_packs.append(pack); + seen_in_batch.insert(pack->addonId.toString()); + } + } + catch (const JSONValidationError&) + { + continue; + } + } + + m_project_batch_offset += batch.size(); + setProgress(getProgress() + 1, getTotalProgress()); + QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchProjectBatch, Qt::QueuedConnection); + }); + connect(job.get(), &Task::failed, this, &ModrinthCollectionImportTask::emitFailed); + connect(job.get(), &Task::aborted, this, &ModrinthCollectionImportTask::emitAborted); + connect(job.get(), &Task::stepProgress, this, &ModrinthCollectionImportTask::propagateStepProgress); + + m_current_task = job; + job->start(); +} + +void ModrinthCollectionImportTask::fetchNextVersion() +{ + if (m_version_index >= m_packs.size()) + { + finishImport(); + return; + } + + auto pack = m_packs.at(m_version_index); + setStatus(tr("Resolving a compatible version for %1...").arg(pack->name)); + + ResourceAPI::Callback> callbacks; + callbacks.on_succeed = [this, pack](auto& versions) + { + if (!versions.isEmpty()) + m_imported_resources.append({ pack, versions.first() }); + else + m_skipped_resources.append(pack->name); + + ++m_version_index; + setProgress(getProgress() + 1, getTotalProgress()); + QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection); + }; + callbacks.on_fail = [this, pack](QString const&, int) + { + m_skipped_resources.append(pack->name); + ++m_version_index; + setProgress(getProgress() + 1, getTotalProgress()); + QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection); + }; + callbacks.on_abort = [this] { emitAborted(); }; + + ResourceAPI::VersionSearchArgs args{ pack, m_mc_versions, m_loaders, ModPlatform::ResourceType::Mod }; + auto job = m_api.getProjectVersions(std::move(args), std::move(callbacks)); + if (!job) + { + m_skipped_resources.append(pack->name); + ++m_version_index; + setProgress(getProgress() + 1, getTotalProgress()); + QMetaObject::invokeMethod(this, &ModrinthCollectionImportTask::fetchNextVersion, Qt::QueuedConnection); + return; + } + + connect(job.get(), &Task::stepProgress, this, &ModrinthCollectionImportTask::propagateStepProgress); + + m_current_task = job; + job->start(); +} + +void ModrinthCollectionImportTask::finishImport() +{ + if (m_imported_resources.isEmpty()) + { + emitFailed(tr("No compatible mods were found in this collection for the current instance.")); + return; + } + + if (!m_skipped_resources.isEmpty()) + { + logWarning(tr("Skipped %1 project(s) without a compatible version: %2") + .arg(m_skipped_resources.size()) + .arg(m_skipped_resources.join(", "))); + } + + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h new file mode 100644 index 0000000000..06618f475c --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthCollectionImportTask.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== + */ + +#pragma once + +#include "Version.h" +#include "modplatform/ModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "tasks/Task.h" + +#include +#include + +#include + +class MinecraftInstance; + +class ModrinthCollectionImportTask final : public Task +{ + Q_OBJECT + + public: + struct ImportedResource + { + ModPlatform::IndexedPack::Ptr pack; + ModPlatform::IndexedVersion version; + }; + + explicit ModrinthCollectionImportTask(QString collection_reference, MinecraftInstance* instance); + + bool abort() override; + + QString collectionName() const + { + return m_collection_name; + } + + QList importedResources() const + { + return m_imported_resources; + } + + QStringList skippedResources() const + { + return m_skipped_resources; + } + + protected: + void executeTask() override; + + private: + QString normalizeCollectionReference(QString reference) const; + QString collectionUrl() const; + bool extractCollectionMetadata(const QByteArray& html); + void fetchCollectionPage(); + void fetchProjectBatch(); + void fetchNextVersion(); + void finishImport(); + + private: + QString m_collection_reference; + QString m_collection_name; + MinecraftInstance* m_instance = nullptr; + + std::list m_mc_versions; + ModPlatform::ModLoaderTypes m_loaders = {}; + + QStringList m_project_ids; + int m_project_batch_offset = 0; + int m_version_index = 0; + + QList m_packs; + QList m_imported_resources; + QStringList m_skipped_resources; + + ModrinthAPI m_api; + shared_qobject_ptr m_current_task; +}; diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp new file mode 100644 index 0000000000..7e9c732eb6 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -0,0 +1,668 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "ModrinthInstanceCreationTask.h" + +#include "Application.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "Json.h" + +#include "QObjectPtr.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "minecraft/mod/Mod.hpp" +#include "modplatform/EnsureMetadataTask.h" +#include "modplatform/helpers/OverrideUtils.h" + +#include "net/ChecksumValidator.h" + +#include "net/ApiDownload.h" +#include "net/NetJob.h" +#include "settings/INISettingsObject.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/pages/modplatform/OptionalModDialog.h" + +#include +#include +#include +#include + +#include "tasks/MultipleOptionsTask.h" + +bool ModrinthCreationTask::abort() +{ + if (!canAbort()) + return false; + + if (m_task) + m_task->abort(); + return InstanceCreationTask::abort(); +} + +bool ModrinthCreationTask::updateInstance() +{ + auto instance_list = APPLICATION->instances(); + + // Note: Duplicate modpack detection uses managed name or instance ID lookup. + // If multiple installations exist, the first match is updated. + InstancePtr inst; + if (auto original_id = originalInstanceID(); !original_id.isEmpty()) + { + inst = instance_list->getInstanceById(original_id); + Q_ASSERT(inst); + } + else + { + // Duplicate Detection: Check for duplicates before assuming + auto all_instances = instance_list->getAllInstancesByManagedName(originalName()); + + if (all_instances.size() > 1) + { + emitFailed(tr("Multiple instances found for this modpack. Please update the specific instance you want to " + "modify to avoid ambiguity.")); + return false; + } + + if (all_instances.size() == 1) + { + inst = all_instances.first(); + } + else + { + // Fallback to name-based lookup if not found by managed ID (e.g. legacy/broken instances) + inst = instance_list->getInstanceById(originalName()); + } + + if (!inst) + { + // New instance creation flow continues... + return true; + } + } + + QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (!parseManifest(index_path, m_files, true, false)) + return false; + + auto version_name = inst->getManagedPackVersionName(); + m_root_path = QFileInfo(inst->gameRoot()).fileName(); + auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; + + if (shouldConfirmUpdate()) + { + auto should_update = askIfShouldUpdate(m_parent, version_str); + if (should_update == ShouldUpdate::SkipUpdating) + return false; + if (should_update == ShouldUpdate::Cancel) + { + m_abort = true; + return false; + } + } + + // Remove repeated files, we don't need to download them! + QDir old_inst_dir(inst->instanceRoot()); + + QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack")); + + QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json")); + QFileInfo old_index_file(old_index_path); + if (old_index_file.exists()) + { + std::vector old_files; + parseManifest(old_index_path, old_files, false, false); + + // Let's remove all duplicated, identical resources! + auto files_iterator = m_files.begin(); + begin: + while (files_iterator != m_files.end()) + { + auto const& file = *files_iterator; + + auto old_files_iterator = old_files.begin(); + while (old_files_iterator != old_files.end()) + { + auto const& old_file = *old_files_iterator; + + if (old_file.hash == file.hash) + { + qDebug() << "Removed file at" << file.path << "from list of downloads"; + files_iterator = m_files.erase(files_iterator); + old_files_iterator = old_files.erase(old_files_iterator); + goto begin; // Sorry :c + } + + old_files_iterator++; + } + + files_iterator++; + } + + QDir old_minecraft_dir(inst->gameRoot()); + + // Some files were removed from the old version, and some will be downloaded in an updated version, + // so we're fine removing them! + if (!old_files.empty()) + { + for (auto const& file : old_files) + { + if (file.path.isEmpty()) + continue; + qDebug() << "Scheduling" << file.path << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path)); + if (file.path.endsWith(".disabled")) + { // remove it if it was enabled/disabled by user + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path.chopped(9))); + } + else + { + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path + ".disabled")); + } + } + } + + // We will remove all the previous overrides, to prevent duplicate files! + // Note: Overrides intentionally replace all files on update - this matches modpack author expectations. + // Disabled mods (.disabled extension) are handled by the file exclusion logic above. + auto old_overrides = Override::readOverrides("overrides", old_index_folder); + for (const auto& entry : old_overrides) + { + if (entry.isEmpty()) + continue; + + // Skip removal of .disabled files (user-disabled mods should be preserved) + if (entry.endsWith(".disabled", Qt::CaseInsensitive)) + { + qDebug() << "Preserving disabled mod:" << entry; + continue; + } + + qDebug() << "Scheduling" << entry << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + } + + auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); + for (const auto& entry : old_client_overrides) + { + if (entry.isEmpty()) + continue; + + // Skip removal of .disabled files (user-disabled mods should be preserved) + if (entry.endsWith(".disabled", Qt::CaseInsensitive)) + { + qDebug() << "Preserving disabled mod:" << entry; + continue; + } + + qDebug() << "Scheduling" << entry << "for removal"; + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + } + } + else + { + // We don't have an old index file, so we may duplicate stuff! + auto dialog = CustomMessageBox::selectable( + m_parent, + tr("No index file."), + tr("We couldn't find a suitable index file for the older version. This may cause some " + "of the files to be duplicated. Do you want to continue?"), + QMessageBox::Warning, + QMessageBox::Ok | QMessageBox::Cancel); + + if (dialog->exec() == QDialog::DialogCode::Rejected) + { + m_abort = true; + return false; + } + } + + setOverride(true, inst->id()); + qDebug() << "Will override instance!"; + + m_instance = inst; + + // We let it go through the createInstance() stage, just with a couple modifications for updating + return false; +} + +// https://docs.modrinth.com/docs/modpacks/format_definition/ +std::unique_ptr ModrinthCreationTask::createInstance() +{ + QEventLoop loop; + + QString parent_folder(FS::PathCombine(m_stagingPath, "mrpack")); + + QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (m_files.empty() && !parseManifest(index_path, m_files, true, true)) + return nullptr; + + // Keep index file in case we need it some other time (like when changing versions) + QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); + FS::ensureFilePathExists(new_index_place); + FS::move(index_path, new_index_place); + + auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); + + auto override_path = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(override_path)) + { + // Create a list of overrides in "overrides.txt" inside mrpack/ + Override::createOverrides("overrides", parent_folder, override_path); + + // Apply the overrides + if (!FS::move(override_path, mcPath)) + { + setError(tr("Could not rename the overrides folder:\n") + "overrides"); + return nullptr; + } + } + + // Do client overrides + auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); + if (QFile::exists(client_override_path)) + { + // Create a list of overrides in "client-overrides.txt" inside mrpack/ + Override::createOverrides("client-overrides", parent_folder, client_override_path); + + // Apply the overrides + if (!FS::overrideFolder(mcPath, client_override_path)) + { + setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); + return nullptr; + } + } + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + auto createdInstance = std::make_unique(m_globalSettings, instanceSettings, m_stagingPath); + auto& instance = *createdInstance; + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_minecraft_version, true); + + if (!m_fabric_version.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version); + if (!m_quilt_version.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); + if (!m_forge_version.isEmpty()) + components->setComponentVersion("net.minecraftforge", m_forge_version); + if (!m_neoForge_version.isEmpty()) + components->setComponentVersion("net.neoforged", m_neoForge_version); + + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + else if (!m_managed_id.isEmpty()) + { + instance.setIconKey("modrinth"); + } + + // Don't add managed info to packs without an ID (most likely imported from ZIP) + if (!m_managed_id.isEmpty()) + instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); + else + instance.setManagedPack("modrinth", "", name(), "", ""); + + instance.setName(name()); + instance.saveNow(); + + auto downloadMods = makeShared(tr("Mod Download Modrinth"), APPLICATION->network()); + + // Clear any previous alternative URLs tracking + m_alternativeUrls.clear(); + + auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); + auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); + // Note: Currently handles mods, resource packs, and shader packs. + // Additional resource types can be added by extending the path prefix checks below. + QHash resources; + for (auto& file : m_files) + { + auto fileName = file.path; + fileName = FS::RemoveInvalidPathChars(fileName); + auto file_path = FS::PathCombine(root_modpack_path, fileName); + if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) + { + // This means we somehow got out of the root folder, so abort here to prevent exploits + setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk " + "and isn't allowed.") + .arg(fileName)); + return nullptr; + } + if (fileName.startsWith("mods/")) + { + auto mod = new Mod(file_path); + ModDetails d; + d.mod_id = file_path; + mod->setDetails(d); + resources[file.hash.toHex()] = mod; + } + if (file.downloads.empty()) + { + setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); + return nullptr; + } + + auto fileTask = makeShared(tr("Download %1").arg(fileName)); + + for (const auto& url : file.downloads) + { + auto dl = Net::ApiDownload::makeFile(QUrl(url.toString()), file_path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + fileTask->addTask(dl); + } + + downloadMods->addTask(fileTask); + } + + bool ended_well = false; + + connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; }); + connect(downloadMods.get(), + &NetJob::failed, + [this, &ended_well](const QString& reason) + { + ended_well = false; + setError(reason); + }); + connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); + connect(downloadMods.get(), + &NetJob::progress, + [this](qint64 current, qint64 total) + { + setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); + setProgress(current, total); + }); + connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); + + setStatus(tr("Downloading mods...")); + downloadMods->start(); + m_task = downloadMods; + + loop.exec(); + + if (!ended_well) + { + for (auto resource : resources) + { + delete resource; + } + return nullptr; + } + + QEventLoop ensureMetaLoop; + QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); + auto ensureMetadataTask = + makeShared(resources, folder, ModPlatform::ResourceProvider::MODRINTH); + connect(ensureMetadataTask.get(), &Task::succeeded, this, [&ended_well]() { ended_well = true; }); + connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); + connect(ensureMetadataTask.get(), + &Task::progress, + [this](qint64 current, qint64 total) + { + setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); + setProgress(current, total); + }); + connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); + + ensureMetadataTask->start(); + m_task = ensureMetadataTask; + + ensureMetaLoop.exec(); + for (auto resource : resources) + { + delete resource; + } + resources.clear(); + + // Update information of the already installed instance, if any. + if (m_instance && ended_well) + { + setAbortable(false); + auto inst = m_instance.value(); + + // Only change the name if it didn't use a custom name, so that the previous custom name + // is preserved, but if we're using the original one, we update the version string. + // NOTE: This needs to come before the copyManagedPack call! + if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance.name()) + { + if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) + inst->setName(instance.name()); + } + + inst->copyManagedPack(instance); + } + + if (ended_well) + { + return createdInstance; + } + return nullptr; +} + +bool ModrinthCreationTask::parseManifest(const QString& index_path, + std::vector& files, + bool set_internal_data, + bool show_optional_dialog) +{ + try + { + auto doc = Json::requireDocument(index_path); + auto obj = Json::requireObject(doc, "modrinth.index.json"); + int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); + if (formatVersion == 1) + { + auto game = Json::requireString(obj, "game", "modrinth.index.json"); + if (game != "minecraft") + { + throw JSONValidationError("Unknown game: " + game); + } + + if (set_internal_data) + { + if (m_managed_version_id.isEmpty()) + m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); + m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name"); + } + + auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); + std::vector optionalFiles; + for (const auto& modInfo : jsonFiles) + { + File file; + file.path = Json::requireString(modInfo, "path").replace("\\", "/"); + + auto env = Json::ensureObject(modInfo, "env"); + // 'env' field is optional + if (!env.isEmpty()) + { + QString support = Json::ensureString(env, "client", "unsupported"); + if (support == "unsupported") + { + continue; + } + else if (support == "optional") + { + file.required = false; + } + } + + QJsonObject hashes = Json::requireObject(modInfo, "hashes"); + file.hash = QByteArray::fromHex(Json::requireString(hashes, "sha512").toLatin1()); + file.hashAlgorithm = QCryptographicHash::Sha512; + + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode + // (as Modrinth seems to incorrectly handle spaces) + + auto download_arr = Json::ensureArray(modInfo, "downloads"); + for (auto download : download_arr) + { + qWarning() << download.toString(); + bool is_last = download.toString() == download_arr.last().toString(); + + auto download_url = QUrl(download.toString()); + + if (!download_url.isValid()) + { + qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL") + .arg(download_url.toString(), file.path); + if (is_last && file.downloads.isEmpty()) + throw JSONValidationError( + tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); + } + else + { + file.downloads.push_back(download_url); + } + } + + (file.required ? files : optionalFiles).push_back(file); + } + + if (!optionalFiles.empty()) + { + if (show_optional_dialog) + { + QStringList oFiles; + for (auto file : optionalFiles) + oFiles.push_back(file.path); + OptionalModDialog optionalModDialog(m_parent, oFiles); + if (optionalModDialog.exec() == QDialog::Rejected) + { + emitAborted(); + return false; + } + + auto selectedMods = optionalModDialog.getResult(); + for (auto file : optionalFiles) + { + if (selectedMods.contains(file.path)) + { + file.required = true; + } + else + { + file.path += ".disabled"; + } + files.push_back(file); + } + } + else + { + for (auto file : optionalFiles) + { + file.path += ".disabled"; + files.push_back(file); + } + } + } + if (set_internal_data) + { + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) + { + QString name = it.key(); + if (name == "minecraft") + { + m_minecraft_version = Json::requireString(*it, "Minecraft version"); + } + else if (name == "fabric-loader") + { + m_fabric_version = Json::requireString(*it, "Fabric Loader version"); + } + else if (name == "quilt-loader") + { + m_quilt_version = Json::requireString(*it, "Quilt Loader version"); + } + else if (name == "forge") + { + m_forge_version = Json::requireString(*it, "Forge version"); + } + else if (name == "neoforge") + { + m_neoForge_version = Json::requireString(*it, "NeoForge version"); + } + else + { + throw JSONValidationError("Unknown dependency type: " + name); + } + } + } + } + else + { + throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); + } + } + catch (const JSONValidationError& e) + { + setError(tr("Could not understand pack index:\n") + e.cause()); + return false; + } + + return true; +} + +void ModrinthCreationTask::attachRetryHandler(Net::Download::Ptr dl, shared_qobject_ptr downloadMods) +{ + // Connect to failed signal for retry logic + connect(dl.get(), + &Task::failed, + this, + [this, downloadMods, dl](QString reason) + { + Q_UNUSED(reason); + auto it = m_alternativeUrls.find(dl.get()); + if (it == m_alternativeUrls.end()) + { + return; + } + + FileDownloadInfo info = it.value(); + if (info.remainingUrls.isEmpty()) + { + m_alternativeUrls.remove(dl.get()); + return; + } + + QString nextUrl = info.remainingUrls.dequeue(); + qDebug() << "Retrying download with alternative URL:" << nextUrl; + + auto newDl = Net::ApiDownload::makeFile(QUrl(nextUrl), info.filePath); + newDl->addValidator(new Net::ChecksumValidator(info.hashAlgorithm, info.hash)); + + if (!info.remainingUrls.isEmpty()) + { + m_alternativeUrls[newDl.get()] = info; + } + + m_alternativeUrls.remove(dl.get()); + attachRetryHandler(newDl, downloadMods); + downloadMods->addNetAction(newDl); + }); + + // Clean up map entry on success + connect(dl.get(), &Task::succeeded, this, [this, dl]() { m_alternativeUrls.remove(dl.get()); }); +} diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h new file mode 100644 index 0000000000..94ca441803 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "BaseInstance.h" +#include "InstanceCreationTask.h" +#include "net/Download.h" + +class NetJob; + +class ModrinthCreationTask final : public InstanceCreationTask +{ + Q_OBJECT + struct File + { + QString path; + + QCryptographicHash::Algorithm hashAlgorithm; + QByteArray hash; + QQueue downloads; + bool required = true; + }; + + public: + ModrinthCreationTask(QString staging_path, + SettingsObjectPtr global_settings, + QWidget* parent, + QString id, + QString version_id = {}, + QString original_instance_id = {}) + : InstanceCreationTask(), + m_parent(parent), + m_managed_id(std::move(id)), + m_managed_version_id(std::move(version_id)) + { + setStagingPath(staging_path); + setParentSettings(global_settings); + + m_original_instance_id = std::move(original_instance_id); + } + + bool abort() override; + + bool updateInstance() override; + std::unique_ptr createInstance() override; + + private: + bool parseManifest(const QString&, + std::vector&, + bool set_internal_data = true, + bool show_optional_dialog = true); + void attachRetryHandler(Net::Download::Ptr dl, shared_qobject_ptr downloadMods); + + private: + QWidget* m_parent = nullptr; + + QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version; + QString m_managed_id, m_managed_version_id, m_managed_name; + + std::vector m_files; + Task::Ptr m_task; + + std::optional m_instance; + + QString m_root_path = "minecraft"; + + // Alternative URLs tracking for download retry mechanism + struct FileDownloadInfo + { + QString filePath; + QQueue remainingUrls; + QByteArray hash; + QCryptographicHash::Algorithm hashAlgorithm; + }; + QHash m_alternativeUrls; +}; diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp new file mode 100644 index 0000000000..e03682f45a --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ModrinthPackExportTask.h" + +#include +#include +#include +#include +#include +#include "Json.h" +#include "MMCZip.h" +#include "minecraft/PackProfile.h" +#include "minecraft/mod/MetadataHandler.hpp" +#include "minecraft/mod/ModFolderModel.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/helpers/HashUtils.h" +#include "tasks/Task.h" + +const QStringList ModrinthPackExportTask::PREFIXES( + { "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); +const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" }); + +ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, + const QString& version, + const QString& summary, + bool optionalFiles, + InstancePtr instance, + const QString& output, + MMCZip::FilterFileFunction filter) + : name(name), + version(version), + summary(summary), + optionalFiles(optionalFiles), + instance(instance), + mcInstance(dynamic_cast(instance.get())), + gameRoot(instance->gameRoot()), + output(output), + filter(filter) +{} + +void ModrinthPackExportTask::executeTask() +{ + setStatus(tr("Searching for files...")); + setProgress(0, 0); + collectFiles(); +} + +bool ModrinthPackExportTask::abort() +{ + if (task) + { + task->abort(); + return true; + } + return false; +} + +void ModrinthPackExportTask::collectFiles() +{ + setAbortable(false); + QCoreApplication::processEvents(); + + files.clear(); + if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) + { + emitFailed(tr("Could not search for files")); + return; + } + + pendingHashes.clear(); + resolvedFiles.clear(); + + if (mcInstance) + { + mcInstance->loaderModList()->update(); + connect(mcInstance->loaderModList().get(), + &ModFolderModel::updateFinished, + this, + &ModrinthPackExportTask::collectHashes); + } + else + collectHashes(); +} + +void ModrinthPackExportTask::collectHashes() +{ + setStatus(tr("Finding file hashes...")); + for (const QFileInfo& file : files) + { + QCoreApplication::processEvents(); + + const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); + // require sensible file types + if (!std::any_of(PREFIXES.begin(), + PREFIXES.end(), + [&relative](const QString& prefix) { return relative.startsWith(prefix); })) + continue; + if (!std::any_of( + FILE_EXTENSIONS.begin(), + FILE_EXTENSIONS.end(), + [&relative](const QString& extension) + { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); })) + continue; + + QFile openFile(file.absoluteFilePath()); + if (!openFile.open(QFile::ReadOnly)) + { + qWarning() << "Could not open" << file << "for hashing"; + continue; + } + + const QByteArray data = openFile.readAll(); + if (openFile.error() != QFileDevice::NoError) + { + qWarning() << "Could not read" << file; + continue; + } + auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512); + + auto allMods = mcInstance->loaderModList()->allMods(); + if (auto modIter = + std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); + modIter != allMods.end()) + { + const Mod* mod = *modIter; + if (mod->metadata() != nullptr) + { + const QUrl& url = mod->metadata()->url; + // ensure the url is permitted on modrinth.com + if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) + { + qDebug() << "Resolving" << relative << "from index"; + + auto sha1 = Hashing::hash(data, Hashing::Algorithm::Sha1); + + ResolvedFile resolvedFile{ sha1, sha512, url.toEncoded(), openFile.size(), mod->metadata()->side }; + resolvedFiles[relative] = resolvedFile; + + // nice! we've managed to resolve based on local metadata! + // no need to enqueue it + continue; + } + } + } + + qDebug() << "Enqueueing" << relative << "for Modrinth query"; + pendingHashes[relative] = sha512; + } + + setAbortable(true); + makeApiRequest(); +} + +void ModrinthPackExportTask::makeApiRequest() +{ + if (pendingHashes.isEmpty()) + buildZip(); + else + { + setStatus(tr("Finding versions for hashes...")); + auto response = std::make_shared(); + task = api.currentVersions(pendingHashes.values(), "sha512", response); + connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); + connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); + connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); + task->start(); + } +} + +void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr response) +{ + task = nullptr; + + try + { + const QJsonDocument doc = Json::requireDocument(*response); + + QMapIterator iterator(pendingHashes); + while (iterator.hasNext()) + { + iterator.next(); + + const QJsonObject obj = doc[iterator.value()].toObject(); + if (obj.isEmpty()) + continue; + + const QJsonArray files_array = obj["files"].toArray(); + if (auto fileIter = std::find_if(files_array.begin(), + files_array.end(), + [&iterator](const QJsonValue& file) + { return file["hashes"]["sha512"] == iterator.value(); }); + fileIter != files_array.end()) + { + // map the file to the url + resolvedFiles[iterator.key()] = + ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), + iterator.value(), + fileIter->toObject()["url"].toString(), + fileIter->toObject()["size"].toInt() }; + } + } + } + catch (const Json::JsonException& e) + { + emitFailed(tr("Failed to parse versions response: %1").arg(e.what())); + return; + } + pendingHashes.clear(); + buildZip(); +} + +void ModrinthPackExportTask::buildZip() +{ + setStatus(tr("Adding files...")); + + auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, true); + zipTask->addExtraFile("modrinth.index.json", generateIndex()); + + zipTask->setExcludeFiles(resolvedFiles.keys()); + + auto progressStep = std::make_shared(); + connect(zipTask.get(), + &Task::finished, + this, + [this, progressStep] + { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(zipTask.get(), &Task::succeeded, this, &ModrinthPackExportTask::emitSucceeded); + connect(zipTask.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); + connect(zipTask.get(), + &Task::failed, + this, + [this, progressStep](QString reason) + { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(zipTask.get(), &Task::stepProgress, this, &ModrinthPackExportTask::propagateStepProgress); + + connect(zipTask.get(), + &Task::progress, + this, + [this, progressStep](qint64 current, qint64 total) + { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(zipTask.get(), + &Task::status, + this, + [this, progressStep](QString status) + { + progressStep->status = status; + stepProgress(*progressStep); + }); + task.reset(zipTask); + zipTask->start(); +} + +QByteArray ModrinthPackExportTask::generateIndex() +{ + QJsonObject out; + out["formatVersion"] = 1; + out["game"] = "minecraft"; + out["name"] = name; + out["versionId"] = version; + if (!summary.isEmpty()) + out["summary"] = summary; + + if (mcInstance) + { + auto profile = mcInstance->getPackProfile(); + // collect all supported components + const ComponentPtr minecraft = profile->getComponent("net.minecraft"); + const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); + const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); + const ComponentPtr forge = profile->getComponent("net.minecraftforge"); + const ComponentPtr neoForge = profile->getComponent("net.neoforged"); + + // convert all available components to mrpack dependencies + QJsonObject dependencies; + if (minecraft != nullptr) + dependencies["minecraft"] = minecraft->m_version; + if (quilt != nullptr) + dependencies["quilt-loader"] = quilt->m_version; + if (fabric != nullptr) + dependencies["fabric-loader"] = fabric->m_version; + if (forge != nullptr) + dependencies["forge"] = forge->m_version; + if (neoForge != nullptr) + dependencies["neoforge"] = neoForge->m_version; + + out["dependencies"] = dependencies; + } + + QJsonArray filesOut; + for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) + { + QJsonObject fileOut; + + QString path = iterator.key(); + const ResolvedFile& value = iterator.value(); + + QJsonObject env; + + // detect disabled mod + const QFileInfo pathInfo(path); + if (optionalFiles && pathInfo.suffix() == "disabled") + { + // rename it + path = pathInfo.dir().filePath(pathInfo.completeBaseName()); + env["client"] = "optional"; + env["server"] = "optional"; + } + else + { + env["client"] = "required"; + env["server"] = "required"; + } + + // a server side mod does not imply that the mod does not work on the client + // however, if a mrpack mod is marked as server-only it will not install on the client + if (iterator->side == ModPlatform::Side::ClientSide) + env["server"] = "unsupported"; + + fileOut["env"] = env; + + fileOut["path"] = path; + fileOut["downloads"] = QJsonArray{ iterator->url }; + + QJsonObject hashes; + hashes["sha1"] = value.sha1; + hashes["sha512"] = value.sha512; + fileOut["hashes"] = hashes; + + fileOut["fileSize"] = value.size; + filesOut << fileOut; + } + out["files"] = filesOut; + + return QJsonDocument(out).toJson(QJsonDocument::Compact); +} diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h new file mode 100644 index 0000000000..95c9d77b22 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include "BaseInstance.h" +#include "MMCZip.h" +#include "minecraft/MinecraftInstance.h" +#include "modplatform/ModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "tasks/Task.h" + +class ModrinthPackExportTask : public Task +{ + Q_OBJECT + public: + ModrinthPackExportTask(const QString& name, + const QString& version, + const QString& summary, + bool optionalFiles, + InstancePtr instance, + const QString& output, + MMCZip::FilterFileFunction filter); + + protected: + void executeTask() override; + bool abort() override; + + private: + struct ResolvedFile + { + QString sha1, sha512, url; + qint64 size; + ModPlatform::Side side; + }; + + static const QStringList PREFIXES; + static const QStringList FILE_EXTENSIONS; + + // inputs + const QString name, version, summary; + const bool optionalFiles; + const InstancePtr instance; + MinecraftInstance* mcInstance; + const QDir gameRoot; + const QString output; + const MMCZip::FilterFileFunction filter; + + ModrinthAPI api; + QFileInfoList files; + QMap pendingHashes; + QMap resolvedFiles; + Task::Ptr task; + + void collectFiles(); + void collectHashes(); + void makeApiRequest(); + void parseApiResponse(std::shared_ptr response); + void buildZip(); + + QByteArray generateIndex(); +}; diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp new file mode 100644 index 0000000000..aecc1879be --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "ModrinthPackIndex.h" +#include "FileSystem.h" +#include "ModrinthAPI.h" + +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "modplatform/ModIndex.h" + +static ModrinthAPI api; + +bool shouldDownloadOnSide(QString side) +{ + return side == "required" || side == "optional"; +} + +// https://docs.modrinth.com/api/operations/getproject/ +void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + pack.addonId = Json::ensureString(obj, "project_id"); + if (pack.addonId.toString().isEmpty()) + pack.addonId = Json::requireString(obj, "id"); + + pack.provider = ModPlatform::ResourceProvider::MODRINTH; + pack.name = Json::requireString(obj, "title"); + + pack.slug = Json::ensureString(obj, "slug", ""); + if (!pack.slug.isEmpty()) + pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; + else + pack.websiteUrl = ""; + + pack.description = Json::ensureString(obj, "description", ""); + + pack.logoUrl = Json::ensureString(obj, "icon_url", ""); + pack.logoName = + QString("%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix()); + + if (obj.contains("author")) + { + ModPlatform::ModpackAuthor modAuthor; + modAuthor.name = Json::ensureString(obj, "author"); + modAuthor.url = api.getAuthorURL(modAuthor.name); + pack.authors = { modAuthor }; + } + + auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side")); + auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side")); + + if (server && client) + { + pack.side = ModPlatform::Side::UniversalSide; + } + else if (server) + { + pack.side = ModPlatform::Side::ServerSide; + } + else if (client) + { + pack.side = ModPlatform::Side::ClientSide; + } + + // Modrinth can have more data than what's provided by the basic search :) + pack.extraDataLoaded = false; +} + +void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url"); + if (pack.extraData.issuesUrl.endsWith('/')) + pack.extraData.issuesUrl.chop(1); + + pack.extraData.sourceUrl = Json::ensureString(obj, "source_url"); + if (pack.extraData.sourceUrl.endsWith('/')) + pack.extraData.sourceUrl.chop(1); + + pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url"); + if (pack.extraData.wikiUrl.endsWith('/')) + pack.extraData.wikiUrl.chop(1); + + pack.extraData.discordUrl = Json::ensureString(obj, "discord_url"); + if (pack.extraData.discordUrl.endsWith('/')) + pack.extraData.discordUrl.chop(1); + + auto donate_arr = Json::ensureArray(obj, "donation_urls"); + for (auto d : donate_arr) + { + auto d_obj = Json::requireObject(d); + + ModPlatform::DonationData donate; + + donate.id = Json::ensureString(d_obj, "id"); + donate.platform = Json::ensureString(d_obj, "platform"); + donate.url = Json::ensureString(d_obj, "url"); + + pack.extraData.donate.append(donate); + } + + pack.extraData.status = Json::ensureString(obj, "status"); + + pack.extraData.body = Json::ensureString(obj, "body").remove("
    "); + + pack.extraDataLoaded = true; +} + +ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, + QString preferred_hash_type, + QString preferred_file_name) +{ + ModPlatform::IndexedVersion file; + + file.addonId = Json::requireString(obj, "project_id"); + file.fileId = Json::requireString(obj, "id"); + file.date = Json::requireString(obj, "date_published"); + auto versionArray = Json::requireArray(obj, "game_versions"); + if (versionArray.empty()) + { + return {}; + } + for (auto mcVer : versionArray) + { + file.mcVersion.append(ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString())); + } + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) + { + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + else if (loader == "risugami") + file.loaders |= ModPlatform::Risugami; + else if (loader == "station-loader") + file.loaders |= ModPlatform::StationLoader; + else if (loader == "modloadermp") + file.loaders |= ModPlatform::ModLoaderMP; + else if (loader == "optifine") + file.loaders |= ModPlatform::Optifine; + } + file.version = Json::requireString(obj, "name"); + file.version_number = Json::requireString(obj, "version_number"); + file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); + + file.changelog = Json::requireString(obj, "changelog"); + + auto dependencies = Json::ensureArray(obj, "dependencies"); + for (auto d : dependencies) + { + auto dep = Json::ensureObject(d); + ModPlatform::Dependency dependency; + dependency.addonId = Json::ensureString(dep, "project_id"); + dependency.version = Json::ensureString(dep, "version_id"); + auto depType = Json::requireString(dep, "dependency_type"); + + if (depType == "required") + dependency.type = ModPlatform::DependencyType::REQUIRED; + else if (depType == "optional") + dependency.type = ModPlatform::DependencyType::OPTIONAL; + else if (depType == "incompatible") + dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; + else if (depType == "embedded") + dependency.type = ModPlatform::DependencyType::EMBEDDED; + else + dependency.type = ModPlatform::DependencyType::UNKNOWN; + + file.dependencies.append(dependency); + } + + auto files = Json::requireArray(obj, "files"); + int i = 0; + + if (files.empty()) + { + // This should not happen normally, but check just in case + qWarning() << "Modrinth returned an unexpected empty list of files:" << obj; + return {}; + } + + // Find correct file (needed in cases where one version may have multiple files) + // Will default to the last one if there's no primary (though I think Modrinth requires that + // at least one file is primary, idk) + // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed + while (i < files.count() - 1) + { + auto parent = files[i].toObject(); + auto fileName = Json::requireString(parent, "filename"); + + if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) + { + file.is_preferred = true; + break; + } + + // Grab the primary file, if available + if (Json::requireBoolean(parent, "primary")) + break; + + i++; + } + + auto parent = files[i].toObject(); + if (parent.contains("url")) + { + file.downloadUrl = Json::requireString(parent, "url"); + file.fileName = Json::requireString(parent, "filename"); + file.fileName = FS::RemoveInvalidPathChars(file.fileName); + file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); + auto hash_list = Json::requireObject(parent, "hashes"); + + if (hash_list.contains(preferred_hash_type)) + { + file.hash = Json::requireString(hash_list, preferred_hash_type); + file.hash_type = preferred_hash_type; + } + else + { + auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH); + for (auto& hash_type : hash_types) + { + if (hash_list.contains(hash_type)) + { + file.hash = Json::requireString(hash_list, hash_type); + file.hash_type = hash_type; + break; + } + } + } + + return file; + } + + return {}; +} diff --git a/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h new file mode 100644 index 0000000000..45f33733ce --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "modplatform/ModIndex.h" + +#include "BaseInstance.h" + +namespace Modrinth +{ + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); + void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); + auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") + -> ModPlatform::IndexedVersion; + +} // namespace Modrinth diff --git a/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp new file mode 100644 index 0000000000..0d2aa5857a --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.cpp @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "Packwiz.h" + +#include +#include +#include +#include +#include +#include + +#include "FileSystem.h" +#include "StringUtils.h" +#include "Version.h" + +#include "modplatform/ModIndex.h" + +#include + +#ifdef Q_OS_WIN32 +#include +#endif + +namespace Packwiz +{ + namespace + { + void sortMinecraftVersionsDescending(QStringList& versions) + { + std::sort(versions.begin(), versions.end(), [](const QString& left, const QString& right) + { return Version(left) > Version(right); }); + } + } // namespace + + auto getRealIndexName(const QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString + { + QFile index_file(index_dir.absoluteFilePath(normalized_fname)); + + QString real_fname = normalized_fname; + if (!index_file.exists()) + { + // Tries to get similar entries + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) + { + if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) + { + real_fname = file_name; + break; + } + } + + if (should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)) + { + qCritical() << "Could not find a match for a valid metadata file!"; + qCritical() << "File: " << normalized_fname; + return {}; + } + } + + return real_fname; + } + + // Helpers + static inline auto indexFileName(const QString& mod_slug) -> QString + { + if (mod_slug.endsWith(".pw.toml")) + return mod_slug; + return QString("%1.pw.toml").arg(mod_slug); + } + + // Helper functions for extracting data from the TOML file + auto stringEntry(toml::table table, QString entry_name) -> QString + { + auto node = table[StringUtils::toStdString(entry_name)]; + if (!node) + { + qWarning() << "Failed to read str property '" + entry_name + "' in mod metadata."; + return {}; + } + + return node.value_or(""); + } + + auto intEntry(toml::table table, QString entry_name) -> int + { + auto node = table[StringUtils::toStdString(entry_name)]; + if (!node) + { + qWarning() << "Failed to read int property '" + entry_name + "' in mod metadata."; + return {}; + } + + return node.value_or(0); + } + + auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, + ModPlatform::IndexedPack& mod_pack, + ModPlatform::IndexedVersion& mod_version) -> Mod + { + Mod mod; + + mod.slug = mod_pack.slug; + mod.name = mod_pack.name; + mod.filename = mod_version.fileName; + + if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) + { + mod.mode = "metadata:curseforge"; + } + else + { + mod.mode = "url"; + mod.url = mod_version.downloadUrl; + } + + mod.hash_format = mod_version.hash_type; + mod.hash = mod_version.hash; + + mod.provider = mod_pack.provider; + mod.file_id = mod_version.fileId; + mod.project_id = mod_pack.addonId; + mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side; + mod.loaders = mod_version.loaders; + mod.mcVersions = mod_version.mcVersion; + sortMinecraftVersionsDescending(mod.mcVersions); + mod.releaseType = mod_version.version_type; + + mod.version_number = mod_version.version_number; + if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number + mod.version_number = mod_version.version; + + return mod; + } + + void V1::updateModIndex(const QDir& index_dir, Mod& mod) + { + // Call with null callback - no user confirmation + updateModIndex(index_dir, mod, nullptr); + } + + void V1::updateModIndex(const QDir& index_dir, + Mod& mod, + std::function confirmOverride) + { + if (!mod.isValid()) + { + qCritical() << QString("Tried to update metadata of an invalid mod!"); + return; + } + + // Ensure the corresponding mod's info exists, and create it if not + + auto normalized_fname = indexFileName(mod.slug); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + if (real_fname != normalized_fname) + index_file.rename(normalized_fname); + + // Logic: Backup existing metadata instead of deleting it, allowing recovery. + if (index_file.exists()) + { + try + { + auto old_metadata = toml::parse_file(index_file.fileName().toStdString()); + auto version_node = old_metadata.at_path("update.flame.file-id"); + + if (version_node) + { + if (auto* str_node = version_node.as_string()) + { + const auto old_version_id = QString::fromStdString(str_node->get()); + + if (old_version_id != mod.file_id.toString()) + { + // Ask user for confirmation if callback is provided + if (confirmOverride) + { + bool shouldOverride = confirmOverride(old_version_id, mod.file_id.toString(), mod.name); + if (!shouldOverride) + { + qDebug() << "User cancelled override for mod" << mod.name; + return; + } + } + + qDebug() << "Updating existing mod" << mod.name << "from version" << old_version_id << "to" + << mod.file_id.toString(); + index_file.remove(); + } + else + { + qDebug() << "Mod" << mod.name << "is already up to date, skipping"; + return; + } + } + else + { + // Node exists but isn't a string -> metadata is weird; recreate safely + qWarning() << "Existing Packwiz metadata has non-string update.flame.file-id for" << mod.name + << "- recreating"; + index_file.remove(); + } + } + } + catch (const toml::parse_error&) + { + // If parsing fails, just remove and recreate + qWarning() << "Failed to parse existing mod index, recreating"; + index_file.remove(); + } + } + else + { + FS::ensureFilePathExists(index_file.fileName()); + } + +#ifdef Q_OS_WIN32 + // `.index` is an internal metadata directory and should be hidden on Windows. + // This covers paths that create it implicitly (e.g. via `ensureFilePathExists`) instead of via + // LocalResourceUpdateTask. + SetFileAttributesW(index_dir.path().toStdWString().c_str(), + FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif + + toml::table update; + switch (mod.provider) + { + case (ModPlatform::ResourceProvider::FLAME): + if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0) + { + qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); + return; + } + update = toml::table{ + { "file-id", mod.file_id.toInt() }, + { "project-id", mod.project_id.toInt() }, + }; + break; + case (ModPlatform::ResourceProvider::MODRINTH): + if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty()) + { + qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); + return; + } + update = toml::table{ + { "mod-id", mod.mod_id().toString().toStdString() }, + { "version", mod.version().toString().toStdString() }, + }; + break; + } + + toml::array loaders; + for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) + { + loaders.push_back(getModLoaderAsString(loader).toStdString()); + } + toml::array mcVersions; + for (auto version : mod.mcVersions) + { + mcVersions.push_back(version.toStdString()); + } + + if (!index_file.open(QIODevice::ReadWrite)) + { + qCritical() << QString("Could not open file %1!").arg(normalized_fname); + return; + } + + // Put TOML data into the file + QTextStream in_stream(&index_file); + { + auto tbl = + toml::table{ { "name", mod.name.toStdString() }, + { "filename", mod.filename.toStdString() }, + { "side", ModPlatform::SideUtils::toString(mod.side).toStdString() }, + { "x-projtlauncher-loaders", loaders }, + { "x-projtlauncher-mc-versions", mcVersions }, + { "x-projtlauncher-release-type", mod.releaseType.toString().toStdString() }, + { "x-projtlauncher-version-number", mod.version_number.toStdString() }, + { "download", + toml::table{ + { "mode", mod.mode.toStdString() }, + { "url", mod.url.toString().toStdString() }, + { "hash-format", mod.hash_format.toStdString() }, + { "hash", mod.hash.toStdString() }, + } }, + { "update", + toml::table{ { ModPlatform::ProviderCapabilities::name(mod.provider), update } } } }; + std::stringstream ss; + ss << tbl; + in_stream << QString::fromStdString(ss.str()); + } + + index_file.flush(); + index_file.close(); + } + + void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug) + { + auto normalized_fname = indexFileName(mod_slug); + auto real_fname = getRealIndexName(index_dir, normalized_fname); + if (real_fname.isEmpty()) + return; + + QFile index_file(index_dir.absoluteFilePath(real_fname)); + + if (!index_file.exists()) + { + qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug); + return; + } + + if (!index_file.remove()) + { + qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug); + } + } + + auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod + { + Mod mod; + + auto normalized_fname = indexFileName(slug); + auto real_fname = getRealIndexName(index_dir, normalized_fname, true); + if (real_fname.isEmpty()) + return {}; + + toml::table table; +#if TOML_EXCEPTIONS + try + { + table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); + } + catch (const toml::parse_error& err) + { + qWarning() << QString("Could not open file %1!").arg(normalized_fname); + qWarning() << "Reason: " << QString(err.what()); + return {}; + } +#else + toml::parse_result result = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); + if (!result) + { + qWarning() << QString("Could not open file %1!").arg(normalized_fname); + qWarning() << "Reason: " << result.error().description(); + return {}; + } + table = result.table(); +#endif + + // index_file.close(); + + mod.slug = slug; + + { // Basic info + mod.name = stringEntry(table, "name"); + mod.filename = stringEntry(table, "filename"); + mod.side = ModPlatform::SideUtils::fromString(stringEntry(table, "side")); + mod.releaseType = ModPlatform::IndexedVersionType(table["x-projtlauncher-release-type"].value_or("")); + if (auto loaders = table["x-projtlauncher-loaders"]; loaders && loaders.is_array()) + { + for (auto&& loader : *loaders.as_array()) + { + if (loader.is_string()) + { + mod.loaders |= ModPlatform::getModLoaderFromString( + QString::fromStdString(loader.as_string()->value_or(""))); + } + } + } + if (auto versions = table["x-projtlauncher-mc-versions"]; versions && versions.is_array()) + { + for (auto&& version : *versions.as_array()) + { + if (version.is_string()) + { + auto ver = QString::fromStdString(version.as_string()->value_or("")); + if (!ver.isEmpty()) + { + mod.mcVersions << ver; + } + } + } + sortMinecraftVersionsDescending(mod.mcVersions); + } + } + mod.version_number = table["x-projtlauncher-version-number"].value_or(""); + + { // [download] info + auto download_table = table["download"].as_table(); + if (!download_table) + { + qCritical() << QString("No [download] section found on mod metadata!"); + return {}; + } + + mod.mode = stringEntry(*download_table, "mode"); + mod.url = stringEntry(*download_table, "url"); + mod.hash_format = stringEntry(*download_table, "hash-format"); + mod.hash = stringEntry(*download_table, "hash"); + } + + { // [update] info + using Provider = ModPlatform::ResourceProvider; + + auto update_table = table["update"]; + if (!update_table || !update_table.is_table()) + { + qCritical() << QString("No [update] section found on mod metadata!"); + return {}; + } + + toml::table* mod_provider_table = nullptr; + if ((mod_provider_table = + update_table[ModPlatform::ProviderCapabilities::name(Provider::FLAME)].as_table())) + { + mod.provider = Provider::FLAME; + mod.file_id = intEntry(*mod_provider_table, "file-id"); + mod.project_id = intEntry(*mod_provider_table, "project-id"); + } + else if ((mod_provider_table = + update_table[ModPlatform::ProviderCapabilities::name(Provider::MODRINTH)].as_table())) + { + mod.provider = Provider::MODRINTH; + mod.mod_id() = stringEntry(*mod_provider_table, "mod-id"); + mod.version() = stringEntry(*mod_provider_table, "version"); + } + else + { + qCritical() << QString("No mod provider on mod metadata!"); + return {}; + } + } + + return mod; + } + + auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod + { + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) + { + auto mod = getIndexForMod(index_dir, file_name); + + if (mod.mod_id() == mod_id) + return mod; + } + + return {}; + } + +} // namespace Packwiz diff --git a/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h new file mode 100644 index 0000000000..a0c136f785 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/packwiz/Packwiz.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "modplatform/ModIndex.h" + +#include +#include +#include + +class QDir; + +namespace Packwiz +{ + + auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; + + class V1 + { + public: + // can also represent other resources beside loader mods - but this is what packwiz calls it + struct Mod + { + QString slug{}; + QString name{}; + QString filename{}; + ModPlatform::Side side{ ModPlatform::Side::UniversalSide }; + ModPlatform::ModLoaderTypes loaders; + QStringList mcVersions; + ModPlatform::IndexedVersionType releaseType; + + // [download] + QString mode{}; + QUrl url{}; + QString hash_format{}; + QString hash{}; + + // [update] + ModPlatform::ResourceProvider provider{}; + QVariant file_id{}; + QVariant project_id{}; + QString version_number{}; + + public: + // This is a totally heuristic, but should work for now. + auto isValid() const -> bool + { + return !slug.isEmpty() && !project_id.isNull(); + } + + // Different providers can use different names for the same thing + // Modrinth-specific + auto mod_id() -> QVariant& + { + return project_id; + } + auto version() -> QVariant& + { + return file_id; + } + }; + + /* Generates the object representing the information in a mod.pw.toml file via + * its common representation in the launcher, when downloading mods. + * */ + static auto createModFormat(const QDir& index_dir, + ModPlatform::IndexedPack& mod_pack, + ModPlatform::IndexedVersion& mod_version) -> Mod; + + /* Updates the mod index for the provided mod. + * This creates a new index if one does not exist already. + * + * If a mod with the same slug already exists: + * - If version differs: old metadata is removed and new one is created + * - If version is the same: update is skipped (mod is already up to date) + * + * The optional callback can be used by UI code to prompt user for override confirmation. + * If callback returns false, the update is cancelled. + * */ + static void updateModIndex(const QDir& index_dir, Mod& mod); + + /** Same as updateModIndex but with override confirmation callback. + * @param confirmOverride Called when an existing different version is found. + * Receives (old_version, new_version, mod_name). Return true to proceed. + */ + static void updateModIndex(const QDir& index_dir, + Mod& mod, + std::function confirmOverride); + + /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(const QDir& index_dir, QString& mod_slug); + + /* Gets the metadata for a mod with a particular file name. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(const QDir& index_dir, QString slug) -> Mod; + + /* Gets the metadata for a mod with a particular id. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod; + }; + +} // namespace Packwiz diff --git a/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp new file mode 100644 index 0000000000..ae50295a4d --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#include "SingleZipPackInstallTask.h" + +#include + +#include "FileSystem.h" +#include "MMCZip.h" +#include "TechnicPackProcessor.h" + +#include "Application.h" + +#include "net/ApiDownload.h" + +Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl& sourceUrl, const QString& minecraftVersion) +{ + m_sourceUrl = sourceUrl; + m_minecraftVersion = minecraftVersion; +} + +bool Technic::SingleZipPackInstallTask::abort() +{ + if (m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + +void Technic::SingleZipPackInstallTask::executeTask() +{ + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + + const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); + entry->setStale(true); + m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); + m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); + m_archivePath = entry->getFullPath(); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); + connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); + connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propagateStepProgress); + connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SingleZipPackInstallTask::downloadSucceeded() +{ + m_abortable = false; + + setStatus(tr("Extracting modpack")); + QDir extractDir(FS::PathCombine(m_stagingPath, "minecraft")); + qDebug() << "Attempting to create instance from" << m_archivePath; + + // open the zip and find relevant files in it + m_packZip.reset(new QuaZip(m_archivePath)); + if (!m_packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), + MMCZip::extractSubDir, + m_packZip.get(), + QString(""), + extractDir.absolutePath()); + connect(&m_extractFutureWatcher, + &QFutureWatcher::finished, + this, + &Technic::SingleZipPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, + &QFutureWatcher::canceled, + this, + &Technic::SingleZipPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) +{ + m_abortable = false; + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + m_abortable = true; + setProgress(current / 2, total); +} + +void Technic::SingleZipPackInstallTask::extractFinished() +{ + m_packZip.reset(); + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if (file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser + | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if (origPermissions != permissions) + { + if (!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + auto packProcessor = makeShared(); + connect(packProcessor.get(), + &Technic::TechnicPackProcessor::succeeded, + this, + &Technic::SingleZipPackInstallTask::emitSucceeded); + connect(packProcessor.get(), + &Technic::TechnicPackProcessor::failed, + this, + &Technic::SingleZipPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion); +} + +void Technic::SingleZipPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); +} diff --git a/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h new file mode 100644 index 0000000000..74ecd8801c --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include "InstanceTask.h" +#include "net/NetJob.h" + +#include + +#include +#include +#include + +#include + +namespace Technic +{ + + class SingleZipPackInstallTask : public InstanceTask + { + Q_OBJECT + + public: + SingleZipPackInstallTask(const QUrl& sourceUrl, const QString& minecraftVersion); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + protected: + void executeTask() override; + + private slots: + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + + private: + bool m_abortable = false; + + QUrl m_sourceUrl; + QString m_minecraftVersion; + QString m_archivePath; + NetJob::Ptr m_filesNetJob; + std::unique_ptr m_packZip; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + }; + +} // namespace Technic diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp new file mode 100644 index 0000000000..9caca4be9f --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2021-2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "SolderPackInstallTask.h" + +#include +#include +#include +#include + +#include "SolderPackManifest.h" +#include "TechnicPackProcessor.h" +#include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" + +Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr network, + const QUrl& solderUrl, + const QString& pack, + const QString& version, + const QString& minecraftVersion) +{ + m_solderUrl = solderUrl; + m_pack = pack; + m_version = version; + m_network = network; + m_minecraftVersion = minecraftVersion; +} + +bool Technic::SolderPackInstallTask::abort() +{ + if (m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + +void Technic::SolderPackInstallTask::executeTask() +{ + setStatus(tr("Resolving modpack files")); + + m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); + auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); + m_filesNetJob->addNetAction(Net::ApiDownload::makeByteArray(sourceUrl, m_response)); + + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); + connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + connect(job, &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::fileListSucceeded() +{ + setStatus(tr("Downloading modpack")); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *m_response; + return; + } + auto obj = doc.object(); + + TechnicSolder::PackBuild build; + try + { + TechnicSolder::loadPackBuild(build, obj); + } + catch (const JSONValidationError& e) + { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + m_filesNetJob.reset(); + return; + } + + if (!build.minecraft.isEmpty()) + m_minecraftVersion = build.minecraft; + + m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network)); + + int i = 0; + for (const auto& mod : build.mods) + { + auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); + + auto dl = Net::ApiDownload::makeFile(mod.url, path); + if (!mod.md5.isEmpty()) + { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); + } + m_filesNetJob->addNetAction(dl); + + i++; + } + + m_modCount = build.mods.size(); + + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propagateStepProgress); + connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::downloadSucceeded() +{ + m_abortable = false; + + setStatus(tr("Extracting modpack")); + m_filesNetJob.reset(); + m_extractFuture = QtConcurrent::run( + [this]() + { + int i = 0; + QString extractDir = FS::PathCombine(m_stagingPath, "minecraft"); + FS::ensureFolderPathExists(extractDir); + + while (m_modCount > i) + { + auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); + if (!MMCZip::extractDir(path, extractDir)) + { + return false; + } + i++; + } + return true; + }); + connect(&m_extractFutureWatcher, + &QFutureWatcher::finished, + this, + &Technic::SolderPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, + &QFutureWatcher::canceled, + this, + &Technic::SolderPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void Technic::SolderPackInstallTask::downloadFailed(QString reason) +{ + m_abortable = false; + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + m_abortable = true; + setProgress(current / 2, total); +} + +void Technic::SolderPackInstallTask::downloadAborted() +{ + emitAborted(); + m_filesNetJob.reset(); +} + +void Technic::SolderPackInstallTask::extractFinished() +{ + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if (file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser + | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if (origPermissions != permissions) + { + if (!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + auto packProcessor = makeShared(); + connect(packProcessor.get(), + &Technic::TechnicPackProcessor::succeeded, + this, + &Technic::SolderPackInstallTask::emitSucceeded); + connect(packProcessor.get(), + &Technic::TechnicPackProcessor::failed, + this, + &Technic::SolderPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true); +} + +void Technic::SolderPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); +} diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h new file mode 100644 index 0000000000..b2d37fe3ad --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackInstallTask.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2021-2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace Technic +{ + class SolderPackInstallTask : public InstanceTask + { + Q_OBJECT + public: + explicit SolderPackInstallTask(shared_qobject_ptr network, + const QUrl& solderUrl, + const QString& pack, + const QString& version, + const QString& minecraftVersion); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + protected: + //! Entry point for tasks. + virtual void executeTask() override; + + private slots: + void fileListSucceeded(); + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void downloadAborted(); + void extractFinished(); + void extractAborted(); + + private: + bool m_abortable = false; + + shared_qobject_ptr m_network; + + NetJob::Ptr m_filesNetJob; + QUrl m_solderUrl; + QString m_pack; + QString m_version; + QString m_minecraftVersion; + std::shared_ptr m_response = std::make_shared(); + QTemporaryDir m_outputDir; + int m_modCount; + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; + }; +} // namespace Technic diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp new file mode 100644 index 0000000000..9f5b3c56fc --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "SolderPackManifest.h" + +#include "Json.h" + +namespace TechnicSolder +{ + + void loadPack(Pack& v, QJsonObject& obj) + { + v.recommended = Json::requireString(obj, "recommended"); + v.latest = Json::requireString(obj, "latest"); + + auto builds = Json::requireArray(obj, "builds"); + for (const auto buildRaw : builds) + { + auto build = Json::requireString(buildRaw); + v.builds.append(build); + } + } + + static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) + { + b.name = Json::requireString(obj, "name"); + b.version = Json::ensureString(obj, "version", ""); + b.md5 = Json::requireString(obj, "md5"); + b.url = Json::requireString(obj, "url"); + } + + void loadPackBuild(PackBuild& v, QJsonObject& obj) + { + v.minecraft = Json::requireString(obj, "minecraft"); + + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) + { + auto modObj = Json::requireObject(modRaw); + PackBuildMod mod; + loadPackBuildMod(mod, modObj); + v.mods.append(mod); + } + } + +} // namespace TechnicSolder diff --git a/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h new file mode 100644 index 0000000000..ee10179950 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/SolderPackManifest.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +namespace TechnicSolder +{ + + struct Pack + { + QString recommended; + QString latest; + QList builds; + }; + + void loadPack(Pack& v, QJsonObject& obj); + + struct PackBuildMod + { + QString name; + QString version; + QString md5; + QString url; + }; + + struct PackBuild + { + QString minecraft; + QList mods; + }; + + void loadPackBuild(PackBuild& v, QJsonObject& obj); + +} // namespace TechnicSolder diff --git a/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp new file mode 100644 index 0000000000..753a1910f4 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#include "TechnicPackProcessor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, + const QString& instName, + const QString& instIcon, + const QString& stagingPath, + const QString& minecraftVersion, + [[maybe_unused]] const bool isSolder) +{ + QString minecraftPath = FS::PathCombine(stagingPath, "minecraft"); + QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); + + instance.setName(instName); + + if (instIcon != "default") + { + instance.setIconKey(instIcon); + } + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + QByteArray data; + + QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar"); + QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); + QString fmlMinecraftVersion; + if (QFile::exists(modpackJar)) + { + QuaZip zipFile(modpackJar); + if (!zipFile.open(QuaZip::mdUnzip)) + { + emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); + return; + } + QuaZipDir zipFileRoot(&zipFile, "/"); + if (zipFileRoot.exists("/version.json")) + { + if (zipFileRoot.exists("/fmlversion.properties")) + { + zipFile.setCurrentFile("fmlversion.properties"); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"fmlversion.properties\"!")); + return; + } + QByteArray fmlVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(fmlVersionData); + // If not present, this evaluates to a null string + fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); + } + zipFile.setCurrentFile("version.json", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + if (minecraftVersion.isEmpty()) + { + emit failed( + tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); + return; + } + components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->installJarMods({ modpackJar }); + + // Forge for 1.4.7 and for 1.5.2 require extra libraries. + // Figure out the forge version and add it as a component + // (the code still comes from the jar mod installed above) + if (zipFileRoot.exists("/forgeversion.properties")) + { + zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + // Really shouldn't happen, but error handling shall not be forgotten + emit failed(tr("Unable to open \"forgeversion.properties\"")); + return; + } + QByteArray forgeVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(forgeVersionData); + QString major, minor, revision, build; + major = iniFile["forge.major.number"].toString(); + minor = iniFile["forge.minor.number"].toString(); + revision = iniFile["forge.revision.number"].toString(); + build = iniFile["forge.build.number"].toString(); + + if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty()) + { + emit failed(tr("Invalid \"forgeversion.properties\"!")); + return; + } + + components->setComponentVersion("net.minecraftforge", + major + '.' + minor + '.' + revision + '.' + build); + } + + components->saveNow(); + emit succeeded(); + return; + } + } + else if (QFile::exists(versionJson)) + { + QFile file(versionJson); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + // This is the "Vanilla" modpack, excluded by the search code + components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->saveNow(); + emit succeeded(); + return; + } + + try + { + QJsonDocument doc = Json::requireDocument(data); + QJsonObject root = Json::requireObject(doc, "version.json"); + QString packMinecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); + if (packMinecraftVersion.isEmpty()) + { + if (fmlMinecraftVersion.isEmpty()) + { + emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); + return; + } + packMinecraftVersion = fmlMinecraftVersion; + } + components->setComponentVersion("net.minecraft", packMinecraftVersion, true); + for (auto library : Json::ensureArray(root, "libraries", {})) + { + if (!library.isObject()) + { + continue; + } + + auto libraryObject = Json::ensureObject(library, {}, ""); + auto libraryName = Json::ensureString(libraryObject, "name", "", ""); + + if (libraryName.startsWith("net.neoforged.fancymodloader:")) + { // it is neoforge + // no easy way to get the version from the libs so use the arguments + auto arguments = Json::ensureObject(root, "arguments", {}); + bool isVersionArg = false; + QString neoforgeVersion; + for (auto arg : Json::ensureArray(arguments, "game", {})) + { + auto argument = Json::ensureString(arg, ""); + if (isVersionArg) + { + neoforgeVersion = argument; + break; + } + else + { + isVersionArg = "--fml.neoForgeVersion" == argument || "--fml.forgeVersion" == argument; + } + } + if (!neoforgeVersion.isEmpty()) + { + components->setComponentVersion("net.neoforged", neoforgeVersion); + } + break; + } + else if ((libraryName.startsWith("net.minecraftforge:forge:") + || libraryName.startsWith("net.minecraftforge:fmlloader:")) + && libraryName.contains('-')) + { + QString libraryVersion = libraryName.section(':', 2); + if (!libraryVersion.startsWith("1.7.10-")) + { + components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1)); + } + else + { + // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 + // part + components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); + } + break; + } + else + { + // -> + static QMap loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" }, + { "net.fabricmc:fabric-loader:", + "net.fabricmc.fabric-loader" }, + { "org.quiltmc:quilt-loader:", "org.quiltmc.quilt-loader" } }; + for (const auto& loader : loaderMap.keys()) + { + if (libraryName.startsWith(loader)) + { + components->setComponentVersion(loaderMap.value(loader), libraryName.section(':', 2)); + break; + } + } + } + } + } + catch (const JSONValidationError& e) + { + emit failed(tr("Could not understand \"version.json\":\n") + e.cause()); + return; + } + + components->saveNow(); + emit succeeded(); +} diff --git a/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h new file mode 100644 index 0000000000..bd9eeab814 --- /dev/null +++ b/archived/projt-launcher/launcher/modplatform/technic/TechnicPackProcessor.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include "settings/SettingsObject.h" + +namespace Technic +{ + // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask + class TechnicPackProcessor : public QObject + { + Q_OBJECT + + signals: + void succeeded(); + void failed(QString reason); + + public: + void run(SettingsObjectPtr globalSettings, + const QString& instName, + const QString& instIcon, + const QString& stagingPath, + const QString& minecraftVersion = QString(), + bool isSolder = false); + }; +} // namespace Technic diff --git a/archived/projt-launcher/launcher/net/ApiDownload.cpp b/archived/projt-launcher/launcher/net/ApiDownload.cpp new file mode 100644 index 0000000000..52602ab5d7 --- /dev/null +++ b/archived/projt-launcher/launcher/net/ApiDownload.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "net/ApiDownload.h" +#include "net/ApiHeaderProxy.h" + +namespace Net +{ + + Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options) + { + auto dl = Download::makeCached(url, entry, options); + dl->addHeaderProxy(new ApiHeaderProxy()); + return dl; + } + + Download::Ptr ApiDownload::makeByteArray(QUrl url, std::shared_ptr output, Download::Options options) + { + auto dl = Download::makeByteArray(url, output, options); + dl->addHeaderProxy(new ApiHeaderProxy()); + return dl; + } + + Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options) + { + auto dl = Download::makeFile(url, path, options); + dl->addHeaderProxy(new ApiHeaderProxy()); + return dl; + } + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/ApiDownload.h b/archived/projt-launcher/launcher/net/ApiDownload.h new file mode 100644 index 0000000000..7e65fdd465 --- /dev/null +++ b/archived/projt-launcher/launcher/net/ApiDownload.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Download.h" + +namespace Net +{ + + namespace ApiDownload + { + Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Download::Options options = Download::Option::NoOptions); + Download::Ptr makeByteArray(QUrl url, + std::shared_ptr output, + Download::Options options = Download::Option::NoOptions); + Download::Ptr makeFile(QUrl url, QString path, Download::Options options = Download::Option::NoOptions); + }; // namespace ApiDownload + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/ApiHeaderProxy.h b/archived/projt-launcher/launcher/net/ApiHeaderProxy.h new file mode 100644 index 0000000000..24f94b2391 --- /dev/null +++ b/archived/projt-launcher/launcher/net/ApiHeaderProxy.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Application.h" +#include "BuildConfig.h" +#include "net/HeaderProxy.h" + +namespace Net +{ + + class ApiHeaderProxy : public HeaderProxy + { + public: + ApiHeaderProxy() : HeaderProxy() + {} + virtual ~ApiHeaderProxy() = default; + + public: + virtual QList headers(const QNetworkRequest& request) const override + { + QList hdrs; + if (APPLICATION->capabilities() & Application::SupportsFlame + && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) + { + hdrs.append({ "x-api-key", APPLICATION->getFlameAPIKey().toUtf8() }); + } + else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() + || request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) + { + QString token = APPLICATION->getModrinthAPIToken(); + if (!token.isNull()) + hdrs.append({ "Authorization", token.toUtf8() }); + } + return hdrs; + }; + }; + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/ApiUpload.cpp b/archived/projt-launcher/launcher/net/ApiUpload.cpp new file mode 100644 index 0000000000..62732c2c03 --- /dev/null +++ b/archived/projt-launcher/launcher/net/ApiUpload.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "net/ApiUpload.h" +#include "net/ApiHeaderProxy.h" + +namespace Net +{ + + Upload::Ptr ApiUpload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) + { + auto up = Upload::makeByteArray(url, output, m_post_data); + up->addHeaderProxy(new ApiHeaderProxy()); + return up; + } + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/ApiUpload.h b/archived/projt-launcher/launcher/net/ApiUpload.h new file mode 100644 index 0000000000..2c7f6984e9 --- /dev/null +++ b/archived/projt-launcher/launcher/net/ApiUpload.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Upload.h" + +namespace Net +{ + + namespace ApiUpload + { + Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); + }; + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/ByteArraySink.h b/archived/projt-launcher/launcher/net/ByteArraySink.h new file mode 100644 index 0000000000..5f64d9b14a --- /dev/null +++ b/archived/projt-launcher/launcher/net/ByteArraySink.h @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "Sink.h" + +namespace Net +{ + + /* + * Sink object for downloads that uses an external QByteArray it doesn't own as a target. + */ + class ByteArraySink : public Sink + { + public: + ByteArraySink(std::shared_ptr output) : m_output(output) {}; + + virtual ~ByteArraySink() = default; + + public: + auto init(QNetworkRequest& request) -> Task::State override + { + if (m_output) + m_output->clear(); + else + qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; + if (initAllValidators(request)) + return Task::State::Running; + m_fail_reason = "Failed to initialize validators"; + return Task::State::Failed; + }; + + auto write(QByteArray& data) -> Task::State override + { + if (m_output) + m_output->append(data); + else + qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; + if (writeAllValidators(data)) + return Task::State::Running; + m_fail_reason = "Failed to write validators"; + return Task::State::Failed; + } + + auto abort() -> Task::State override + { + failAllValidators(); + m_fail_reason = "Aborted"; + return Task::State::Failed; + } + + auto finalize(QNetworkReply& reply) -> Task::State override + { + if (finalizeAllValidators(reply)) + return Task::State::Succeeded; + m_fail_reason = "Failed to finalize validators"; + return Task::State::Failed; + } + + auto hasLocalData() -> bool override + { + return false; + } + + protected: + std::shared_ptr m_output; + }; +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/ChecksumValidator.h b/archived/projt-launcher/launcher/net/ChecksumValidator.h new file mode 100644 index 0000000000..73f8d31fc9 --- /dev/null +++ b/archived/projt-launcher/launcher/net/ChecksumValidator.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "Validator.h" + +#include +#include + +namespace Net +{ + class ChecksumValidator : public Validator + { + public: + ChecksumValidator(QCryptographicHash::Algorithm algorithm, QString expectedHex) + : Net::ChecksumValidator(algorithm, QByteArray::fromHex(expectedHex.toLatin1())) + {} + ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) + : m_checksum(algorithm), + m_expected(expected) {}; + virtual ~ChecksumValidator() = default; + + public: + auto init(QNetworkRequest&) -> bool override + { + m_checksum.reset(); + return true; + } + + auto write(QByteArray& data) -> bool override + { + m_checksum.addData(data); + return true; + } + + auto abort() -> bool override + { + m_checksum.reset(); + return true; + } + + auto validate(QNetworkReply&) -> bool override + { + if (m_expected.size() && m_expected != hash()) + { + qWarning() << "Checksum mismatch, download is bad."; + return false; + } + return true; + } + + auto hash() -> QByteArray + { + return m_checksum.result(); + } + + void setExpected(QByteArray expected) + { + m_expected = expected; + } + + private: + QCryptographicHash m_checksum; + QByteArray m_expected; + }; +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/Download.cpp b/archived/projt-launcher/launcher/net/Download.cpp new file mode 100644 index 0000000000..c3c05a4b27 --- /dev/null +++ b/archived/projt-launcher/launcher/net/Download.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "Download.h" +#include + +#include +#include +#include + +#include "ByteArraySink.h" +#include "ChecksumValidator.h" +#include "MetaCacheSink.h" + +namespace Net +{ + +#if defined(LAUNCHER_APPLICATION) + auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr + { + auto dl = makeShared(); + dl->m_url = url; + dl->setObjectName(QString("CACHE:") + url.toString()); + dl->m_options = options; + auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); + auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal)); + dl->m_sink.reset(cachedNode); + return dl; + } +#endif + + auto Download::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr + { + auto dl = makeShared(); + dl->m_url = url; + dl->setObjectName(QString("BYTES:") + url.toString()); + dl->m_options = options; + dl->m_sink.reset(new ByteArraySink(output)); + return dl; + } + + auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr + { + auto dl = makeShared(); + dl->m_url = url; + dl->setObjectName(QString("FILE:") + url.toString()); + dl->m_options = options; + dl->m_sink.reset(new FileSink(path)); + return dl; + } + + QNetworkReply* Download::getReply(QNetworkRequest& request) + { + return m_network->get(request); + } +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/Download.h b/archived/projt-launcher/launcher/net/Download.h new file mode 100644 index 0000000000..e3789fdc81 --- /dev/null +++ b/archived/projt-launcher/launcher/net/Download.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "HttpMetaCache.h" + +#include "QObjectPtr.h" +#include "net/NetRequest.h" + +namespace Net +{ + class Download : public NetRequest + { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + explicit Download() : NetRequest() + { + logCat = taskDownloadLogC; + } + +#if defined(LAUNCHER_APPLICATION) + static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; +#endif + + static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) + -> Download::Ptr; + static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + }; +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/FileSink.cpp b/archived/projt-launcher/launcher/net/FileSink.cpp new file mode 100644 index 0000000000..0d900feed4 --- /dev/null +++ b/archived/projt-launcher/launcher/net/FileSink.cpp @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "FileSink.h" + +#include "FileSystem.h" + +#include "net/Logging.h" + +namespace Net +{ + + Task::State FileSink::init(QNetworkRequest& request) + { + auto result = initCache(request); + if (result != Task::State::Running) + { + return result; + } + + // create a new save file and open it for writing + if (!FS::ensureFilePathExists(m_filename)) + { + qCCritical(taskNetLogC) << "Could not create folder for " + m_filename; + m_fail_reason = "Could not create folder"; + return Task::State::Failed; + } + + m_wroteAnyData = false; + m_output_file.reset(new PSaveFile(m_filename)); + if (!m_output_file->open(QIODevice::WriteOnly)) + { + qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; + m_fail_reason = "Could not open file"; + return Task::State::Failed; + } + + if (initAllValidators(request)) + return Task::State::Running; + m_fail_reason = "Failed to initialize validators"; + return Task::State::Failed; + } + + Task::State FileSink::write(QByteArray& data) + { + if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) + { + qCCritical(taskNetLogC) << "Failed writing into " + m_filename; + m_output_file->cancelWriting(); + m_output_file.reset(); + m_wroteAnyData = false; + m_fail_reason = "Failed to write validators"; + return Task::State::Failed; + } + + m_wroteAnyData = true; + return Task::State::Running; + } + + Task::State FileSink::abort() + { + if (m_output_file) + { + m_output_file->cancelWriting(); + } + failAllValidators(); + return Task::State::Failed; + } + + Task::State FileSink::finalize(QNetworkReply& reply) + { + bool gotFile = false; + QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); + bool validStatus = false; + int statusCode = statusCodeV.toInt(&validStatus); + if (validStatus) + { + // this leaves out 304 Not Modified + gotFile = statusCode == 200 || statusCode == 203; + } + + // if we wrote any data to the save file, we try to commit the data to the real file. + // if it actually got a proper file, we write it even if it was empty + if (gotFile || m_wroteAnyData) + { + // ask validators for data consistency + // we only do this for actual downloads, not 'your data is still the same' cache hits + if (!finalizeAllValidators(reply)) + { + m_fail_reason = "Failed to finalize validators"; + return Task::State::Failed; + } + + // nothing went wrong... + if (!m_output_file->commit()) + { + qCCritical(taskNetLogC) << "Failed to commit changes to " << m_filename; + m_output_file->cancelWriting(); + m_fail_reason = "Failed to commit changes"; + return Task::State::Failed; + } + } + + // then get rid of the save file + m_output_file.reset(); + + return finalizeCache(reply); + } + + Task::State FileSink::initCache(QNetworkRequest&) + { + return Task::State::Running; + } + + Task::State FileSink::finalizeCache(QNetworkReply&) + { + return Task::State::Succeeded; + } + + bool FileSink::hasLocalData() + { + QFileInfo info(m_filename); + return info.exists() && info.size() != 0; + } +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/FileSink.h b/archived/projt-launcher/launcher/net/FileSink.h new file mode 100644 index 0000000000..78a4f13b34 --- /dev/null +++ b/archived/projt-launcher/launcher/net/FileSink.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "PSaveFile.h" +#include "Sink.h" + +namespace Net +{ + class FileSink : public Sink + { + public: + FileSink(QString filename) : m_filename(filename) {}; + virtual ~FileSink() = default; + + public: + auto init(QNetworkRequest& request) -> Task::State override; + auto write(QByteArray& data) -> Task::State override; + auto abort() -> Task::State override; + auto finalize(QNetworkReply& reply) -> Task::State override; + + auto hasLocalData() -> bool override; + + protected: + virtual auto initCache(QNetworkRequest&) -> Task::State; + virtual auto finalizeCache(QNetworkReply& reply) -> Task::State; + + protected: + QString m_filename; + bool m_wroteAnyData = false; + std::unique_ptr m_output_file; + }; +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/HeaderProxy.h b/archived/projt-launcher/launcher/net/HeaderProxy.h new file mode 100644 index 0000000000..8ad8befdd8 --- /dev/null +++ b/archived/projt-launcher/launcher/net/HeaderProxy.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +namespace Net +{ + + struct HeaderPair + { + QByteArray headerName; + QByteArray headerValue; + }; + + class HeaderProxy + { + public: + HeaderProxy() + {} + virtual ~HeaderProxy() + {} + + public: + virtual QList headers(const QNetworkRequest& request) const = 0; + + public: + void writeHeaders(QNetworkRequest& request) + { + for (auto header : headers(request)) + { + request.setRawHeader(header.headerName, header.headerValue); + } + } + }; + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/HttpMetaCache.cpp b/archived/projt-launcher/launcher/net/HttpMetaCache.cpp new file mode 100644 index 0000000000..f3719389be --- /dev/null +++ b/archived/projt-launcher/launcher/net/HttpMetaCache.cpp @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "HttpMetaCache.h" +#include "FileSystem.h" +#include "Json.h" + +#include +#include +#include +#include + +#include + +#include "net/Logging.h" + +auto MetaEntry::getFullPath() -> QString +{ + QString fullPath = FS::PathCombine(m_basePath, m_relativePath); + QFileInfo info(fullPath); + if (!info.exists()) + { + qWarning() << "MetaEntry::getFullPath: Path does not exist:" << fullPath; + } + return fullPath; +} + +HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) +{ + saveBatchingTimer.setSingleShot(true); + saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); + + connect(&saveBatchingTimer, &QTimer::timeout, this, &HttpMetaCache::SaveNow); +} + +HttpMetaCache::~HttpMetaCache() +{ + saveBatchingTimer.stop(); + SaveNow(); +} + +auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr +{ + // no base. no base path. can't store + if (!m_entries.contains(base)) + { + qWarning() << "HttpMetaCache::getEntry: base not found:" << base << "resource_path:" << resource_path; + return {}; + } + + EntryMap& map = m_entries[base]; + if (map.entry_list.contains(resource_path)) + { + return map.entry_list[resource_path]; + } + + return {}; +} + +auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr +{ + resource_path = FS::RemoveInvalidPathChars(resource_path); + auto entry = getEntry(base, resource_path); + // it's not present? generate a default stale entry + if (!entry) + { + return staleEntry(base, resource_path); + } + + auto& selected_base = m_entries[base]; + QString real_path = FS::PathCombine(selected_base.base_path, resource_path); + QFileInfo finfo(real_path); + + // is the file really there? if not -> stale + if (!finfo.isFile() || !finfo.isReadable()) + { + // if the file doesn't exist, we disown the entry + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + + if (!expected_etag.isEmpty() && expected_etag != entry->m_etag) + { + // if the etag doesn't match expected, we disown the entry + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + + // if the file changed, check md5sum + qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); + if (file_last_changed != entry->m_local_changed_timestamp) + { + QFile input(real_path); + if (!input.open(QIODevice::ReadOnly)) + { + qWarning() << "Failed to open file '" << input.fileName() << "' for reading!"; + return staleEntry(base, resource_path); + } + QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); + if (entry->m_md5sum != md5sum) + { + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + + // md5sums matched... keep entry and save the new state to file + entry->m_local_changed_timestamp = file_last_changed; + SaveEventually(); + } + + // Get rid of old entries, to prevent cache problems + // DISABLED: This was causing 500+ms delay when opening NewInstanceDialog + // Users can manually clear cache if needed + /* + auto current_time = QDateTime::currentSecsSinceEpoch(); + if (entry->isExpired(current_time - (file_last_changed / 1000))) { + qCWarning(taskNetLogC) << "[HttpMetaCache]" + << "Removing cache entry because of old age!"; + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + */ + + // entry passed all the checks we cared about. + entry->m_basePath = getBasePath(base); + return entry; +} + +auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool +{ + if (!m_entries.contains(stale_entry->m_baseId)) + { + qCCritical(taskHttpMetaCacheLogC) + << "Cannot add entry with unknown base: " << stale_entry->m_baseId.toLocal8Bit(); + return false; + } + + if (stale_entry->m_stale) + { + qCCritical(taskHttpMetaCacheLogC) << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); + return false; + } + + m_entries[stale_entry->m_baseId].entry_list[stale_entry->m_relativePath] = stale_entry; + SaveEventually(); + + return true; +} + +auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool +{ + if (!entry) + return false; + + entry->m_stale = true; + SaveEventually(); + return true; +} + +// returns true on success, false otherwise +auto HttpMetaCache::evictAll() -> bool +{ + bool ret = true; + for (QString& base : m_entries.keys()) + { + EntryMap& map = m_entries[base]; + qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base; + for (MetaEntryPtr entry : map.entry_list) + { + if (!evictEntry(entry)) + qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; + } + map.entry_list.clear(); + // AND all return codes together so the result is true iff all runs of deletePath() are true + ret &= FS::deletePath(map.base_path); + } + return ret; +} + +auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr +{ + auto foo = new MetaEntry(); + foo->m_baseId = base; + foo->m_basePath = getBasePath(base); + foo->m_relativePath = resource_path; + foo->m_stale = true; + + return MetaEntryPtr(foo); +} + +void HttpMetaCache::addBase(QString base, QString base_root) +{ + if (m_entries.contains(base)) + { + qWarning() << "Base" << base << "already exists in meta cache"; + return; + } + + // Check if the base path is valid + QDir baseDir(base_root); + if (!baseDir.exists()) + { + qWarning() << "Base path" << base_root << "does not exist for base" << base; + } + + EntryMap foo; + foo.base_path = base_root; + m_entries[base] = foo; +} + +auto HttpMetaCache::getBasePath(QString base) -> QString +{ + if (m_entries.contains(base)) + { + return m_entries[base].base_path; + } + + return {}; +} + +void HttpMetaCache::Load() +{ + if (m_index_file.isNull()) + return; + + QFile index(m_index_file); + if (!index.open(QIODevice::ReadOnly)) + return; + + QJsonParseError parseError; + QJsonDocument json = QJsonDocument::fromJson(index.readAll(), &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse HttpMetaCache file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return; + } + + // Make sure the root is an object. + if (!json.isObject()) + { + qCritical() << "HttpMetaCache root should be an object."; + return; + } + + auto root = json.object(); + + // check file version first + auto version_val = Json::ensureString(root, "version"); + if (version_val != "1") + return; + + // read the entry array + auto array = Json::ensureArray(root, "entries"); + for (auto element : array) + { + auto element_obj = Json::ensureObject(element); + auto base = Json::ensureString(element_obj, "base"); + if (!m_entries.contains(base)) + continue; + + auto& entrymap = m_entries[base]; + + auto foo = new MetaEntry(); + foo->m_baseId = base; + foo->m_relativePath = Json::ensureString(element_obj, "path"); + foo->m_md5sum = Json::ensureString(element_obj, "md5sum"); + foo->m_etag = Json::ensureString(element_obj, "etag"); + foo->m_local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); + foo->m_remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); + + foo->makeEternal(Json::ensureBoolean(element_obj, (const QString)QStringLiteral("eternal"), false)); + if (!foo->isEternal()) + { + foo->m_current_age = Json::ensureDouble(element_obj, "current_age"); + foo->m_max_age = Json::ensureDouble(element_obj, "max_age"); + } + + // presumed innocent until closer examination + foo->m_stale = false; + + entrymap.entry_list[foo->m_relativePath] = MetaEntryPtr(foo); + } +} + +void HttpMetaCache::SaveEventually() +{ + // reset the save timer + saveBatchingTimer.stop(); + saveBatchingTimer.start(30000); +} + +void HttpMetaCache::SaveNow() +{ + if (m_index_file.isNull()) + return; + + qCDebug(taskHttpMetaCacheLogC) << "Saving metacache with" << m_entries.size() << "entries"; + + QJsonObject toplevel; + Json::writeString(toplevel, "version", "1"); + + QJsonArray entriesArr; + for (auto group : m_entries) + { + for (auto entry : group.entry_list) + { + // do not save stale entries. they are dead. + if (entry->m_stale) + { + continue; + } + + QJsonObject entryObj; + Json::writeString(entryObj, "base", entry->m_baseId); + Json::writeString(entryObj, "path", entry->m_relativePath); + Json::writeString(entryObj, "md5sum", entry->m_md5sum); + Json::writeString(entryObj, "etag", entry->m_etag); + entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->m_local_changed_timestamp))); + if (!entry->m_remote_changed_timestamp.isEmpty()) + entryObj.insert("remote_changed_timestamp", QJsonValue(entry->m_remote_changed_timestamp)); + if (entry->isEternal()) + { + entryObj.insert("eternal", true); + } + else + { + entryObj.insert("current_age", QJsonValue(double(entry->m_current_age))); + entryObj.insert("max_age", QJsonValue(double(entry->m_max_age))); + } + entriesArr.append(entryObj); + } + } + toplevel.insert("entries", entriesArr); + + try + { + Json::write(toplevel, m_index_file); + } + catch (const Exception& e) + { + qCWarning(taskHttpMetaCacheLogC) << "Error writing cache:" << e.what(); + } +} diff --git a/archived/projt-launcher/launcher/net/HttpMetaCache.h b/archived/projt-launcher/launcher/net/HttpMetaCache.h new file mode 100644 index 0000000000..b36993cbf5 --- /dev/null +++ b/archived/projt-launcher/launcher/net/HttpMetaCache.h @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include + +class HttpMetaCache; + +class MetaEntry +{ + friend class HttpMetaCache; + + protected: + MetaEntry() = default; + + public: + auto isStale() -> bool + { + return m_stale; + } + void setStale(bool stale) + { + m_stale = stale; + } + + auto getFullPath() -> QString; + + auto getRemoteChangedTimestamp() -> QString + { + return m_remote_changed_timestamp; + } + void setRemoteChangedTimestamp(QString remote_changed_timestamp) + { + m_remote_changed_timestamp = remote_changed_timestamp; + } + void setLocalChangedTimestamp(qint64 timestamp) + { + m_local_changed_timestamp = timestamp; + } + + auto getETag() -> QString + { + return m_etag; + } + void setETag(QString etag) + { + m_etag = etag; + } + + auto getMD5Sum() -> QString + { + return m_md5sum; + } + void setMD5Sum(QString md5sum) + { + m_md5sum = md5sum; + } + + /* Whether the entry expires after some time (false) or not (true). */ + void makeEternal(bool eternal) + { + m_is_eternal = eternal; + } + bool isEternal() const + { + return m_is_eternal; + } + + auto getCurrentAge() -> qint64 + { + return m_current_age; + } + void setCurrentAge(qint64 age) + { + m_current_age = age; + } + + auto getMaximumAge() -> qint64 + { + return m_max_age; + } + void setMaximumAge(qint64 age) + { + m_max_age = age; + } + + bool isExpired(qint64 offset) + { + return !m_is_eternal && (m_current_age >= m_max_age - offset); + } + + protected: + QString m_baseId; + QString m_basePath; + QString m_relativePath; + QString m_md5sum; + QString m_etag; + + qint64 m_local_changed_timestamp = 0; + QString m_remote_changed_timestamp; // QString for now, RFC 2822 encoded time + qint64 m_current_age = 0; + qint64 m_max_age = 0; + bool m_is_eternal = false; + + bool m_stale = true; +}; + +using MetaEntryPtr = std::shared_ptr; + +class HttpMetaCache : public QObject +{ + Q_OBJECT + public: + // supply path to the cache index file + HttpMetaCache(QString path = QString()); + ~HttpMetaCache() override; + + // get the entry solely from the cache + // you probably don't want this, unless you have some specific caching needs. + auto getEntry(QString base, QString resource_path) -> MetaEntryPtr; + + // get the entry from cache and verify that it isn't stale (within reason) + auto resolveEntry(QString base, QString resource_path, QString expected_etag = QString()) -> MetaEntryPtr; + + // add a previously resolved stale entry + auto updateEntry(MetaEntryPtr stale_entry) -> bool; + + // evict selected entry from cache + auto evictEntry(MetaEntryPtr entry) -> bool; + bool evictAll(); + + void addBase(QString base, QString base_root); + + // (re)start a timer that calls SaveNow later. + void SaveEventually(); + void Load(); + + auto getBasePath(QString base) -> QString; + + public slots: + void SaveNow(); + + private: + // create a new stale entry, given the parameters + auto staleEntry(QString base, QString resource_path) -> MetaEntryPtr; + + struct EntryMap + { + QString base_path; + QMap entry_list; + }; + + QMap m_entries; + QString m_index_file; + QTimer saveBatchingTimer; +}; diff --git a/archived/projt-launcher/launcher/net/Logging.cpp b/archived/projt-launcher/launcher/net/Logging.cpp new file mode 100644 index 0000000000..e2b9603740 --- /dev/null +++ b/archived/projt-launcher/launcher/net/Logging.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "net/Logging.h" + +Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") +Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") +Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") +Q_LOGGING_CATEGORY(taskMCSkinsLogC, "launcher.task.minecraft.skins") +Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") +Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") diff --git a/archived/projt-launcher/launcher/net/Logging.h b/archived/projt-launcher/launcher/net/Logging.h new file mode 100644 index 0000000000..316f0036ef --- /dev/null +++ b/archived/projt-launcher/launcher/net/Logging.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include + +Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) +Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) +Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) +Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC) +Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) +Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) diff --git a/archived/projt-launcher/launcher/net/MetaCacheSink.cpp b/archived/projt-launcher/launcher/net/MetaCacheSink.cpp new file mode 100644 index 0000000000..de7675f6ec --- /dev/null +++ b/archived/projt-launcher/launcher/net/MetaCacheSink.cpp @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "MetaCacheSink.h" +#include +#include +#include +#include "Application.h" + +#include "net/Logging.h" + +namespace Net +{ + +/** Maximum time to hold a cache entry + * = 1 week in seconds + */ +#define MAX_TIME_TO_EXPIRE 1 * 7 * 24 * 60 * 60 + + MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal) + : Net::FileSink(entry->getFullPath()), + m_entry(entry), + m_md5Node(md5sum), + m_is_eternal(is_eternal) + { + addValidator(md5sum); + } + + Task::State MetaCacheSink::initCache(QNetworkRequest& request) + { + if (!m_entry->isStale()) + { + return Task::State::Succeeded; + } + + // check if file exists, if it does, use its information for the request + QFile current(m_filename); + if (current.exists() && current.size() != 0) + { + if (m_entry->getRemoteChangedTimestamp().size()) + { + request.setRawHeader(QString("If-Modified-Since").toLatin1(), + m_entry->getRemoteChangedTimestamp().toLatin1()); + } + if (m_entry->getETag().size()) + { + request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); + } + } + + return Task::State::Running; + } + + Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply) + { + QFileInfo output_file_info(m_filename); + + if (m_wroteAnyData) + { + m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); + } + + m_entry->setETag(reply.rawHeader("ETag").constData()); + + if (reply.hasRawHeader("Last-Modified")) + { + m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData()); + } + + m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); + + { // Cache lifetime + if (m_is_eternal) + { + qCDebug(taskMetaCacheLogC) << "Adding eternal cache entry:" << m_entry->getFullPath(); + m_entry->makeEternal(true); + } + else if (reply.hasRawHeader("Cache-Control")) + { + auto cache_control_header = reply.rawHeader("Cache-Control"); + qCDebug(taskMetaCacheLogC) << "Parsing 'Cache-Control' header with" << cache_control_header; + + static const QRegularExpression s_maxAgeExpr("max-age=([0-9]+)"); + qint64 max_age = s_maxAgeExpr.match(cache_control_header).captured(1).toLongLong(); + m_entry->setMaximumAge(max_age); + } + else if (reply.hasRawHeader("Expires")) + { + auto expires_header = reply.rawHeader("Expires"); + qCDebug(taskMetaCacheLogC) << "Parsing 'Expires' header with" << expires_header; + + qint64 max_age = + QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch(); + m_entry->setMaximumAge(max_age); + } + else + { + m_entry->setMaximumAge(MAX_TIME_TO_EXPIRE); + } + + if (reply.hasRawHeader("Age")) + { + auto age_header = reply.rawHeader("Age"); + qCDebug(taskMetaCacheLogC) << "Parsing 'Age' header with" << age_header; + + qint64 current_age = age_header.toLongLong(); + m_entry->setCurrentAge(current_age); + } + else + { + m_entry->setCurrentAge(0); + } + } + + m_entry->setStale(false); + APPLICATION->metacache()->updateEntry(m_entry); + + return Task::State::Succeeded; + } + + bool MetaCacheSink::hasLocalData() + { + QFileInfo info(m_filename); + return info.exists() && info.size() != 0; + } +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/MetaCacheSink.h b/archived/projt-launcher/launcher/net/MetaCacheSink.h new file mode 100644 index 0000000000..c3c8cc6c3c --- /dev/null +++ b/archived/projt-launcher/launcher/net/MetaCacheSink.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "ChecksumValidator.h" +#include "FileSink.h" +#include "net/HttpMetaCache.h" + +namespace Net +{ + class MetaCacheSink : public FileSink + { + public: + MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal = false); + virtual ~MetaCacheSink() = default; + + auto hasLocalData() -> bool override; + + protected: + auto initCache(QNetworkRequest& request) -> Task::State override; + auto finalizeCache(QNetworkReply& reply) -> Task::State override; + + private: + MetaEntryPtr m_entry; + ChecksumValidator* m_md5Node; + bool m_is_eternal; + }; +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/Mode.h b/archived/projt-launcher/launcher/net/Mode.h new file mode 100644 index 0000000000..8ef9a7f5ab --- /dev/null +++ b/archived/projt-launcher/launcher/net/Mode.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#pragma once + +namespace Net +{ + enum class Mode + { + Offline, + Online + }; +} diff --git a/archived/projt-launcher/launcher/net/NetJob.cpp b/archived/projt-launcher/launcher/net/NetJob.cpp new file mode 100644 index 0000000000..f85bd932b9 --- /dev/null +++ b/archived/projt-launcher/launcher/net/NetJob.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "NetJob.h" +#include +#include "net/NetRequest.h" +#include "tasks/ConcurrentTask.h" +#if defined(LAUNCHER_APPLICATION) +#include "Application.h" +#include "ui/dialogs/CustomMessageBox.h" +#endif + +NetJob::NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent) + : ConcurrentTask(job_name), + m_network(network) +{ +#if defined(LAUNCHER_APPLICATION) + if (APPLICATION_DYN && max_concurrent < 0) + max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt(); +#endif + if (max_concurrent > 0) + setMaxConcurrent(max_concurrent); +} + +auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool +{ + action->setNetwork(m_network); + + addTask(action); + + return true; +} + +void NetJob::executeNextSubTask() +{ + // We're finished, check for failures and retry if we can (up to 3 times) + if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) + { + m_try += 1; + const auto failedTasks = m_failed.keys(); + for (auto* failedTask : failedTasks) + { + auto task = m_failed.take(failedTask); + m_done.remove(task.get()); + m_queue.enqueue(task); + } + } + ConcurrentTask::executeNextSubTask(); +} + +auto NetJob::size() const -> int +{ + return m_queue.size() + m_doing.size() + m_done.size(); +} + +auto NetJob::canAbort() const -> bool +{ + bool canFullyAbort = true; + + // can abort the downloads on the queue? + for (auto part : m_queue) + canFullyAbort &= part->canAbort(); + + // can abort the active downloads? + for (auto part : m_doing) + canFullyAbort &= part->canAbort(); + + return canFullyAbort; +} + +auto NetJob::abort() -> bool +{ + bool fullyAborted = true; + + // fail all downloads on the queue + for (auto task : m_queue) + m_failed.insert(task.get(), task); + m_queue.clear(); + + // abort active downloads + auto toKill = m_doing.values(); + for (auto part : toKill) + { + fullyAborted &= part->abort(); + } + + if (fullyAborted) + emitAborted(); + else + emitFailed(tr("Failed to abort all tasks in the NetJob!")); + + return fullyAborted; +} + +auto NetJob::getFailedActions() -> QList +{ + QList failed; + for (auto index : m_failed) + { + failed.push_back(dynamic_cast(index.get())); + } + return failed; +} + +auto NetJob::getFailedFiles() -> QList +{ + QList failed; + for (auto index : m_failed) + { + failed.append(static_cast(index.get())->url().toString()); + } + return failed; +} + +void NetJob::updateState() +{ + emit progress(m_done.count(), totalSize()); + setStatus( + tr("Executing %1 task(s) (%2 out of %3 are done)") + .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); +} + +bool NetJob::isOnline() +{ + // check some errors that are ussually associated with the lack of internet + for (auto job : getFailedActions()) + { + auto err = job->error(); + if (err != QNetworkReply::HostNotFoundError && err != QNetworkReply::NetworkSessionFailedError) + { + return true; + } + } + return false; +}; + +void NetJob::emitFailed(QString reason) +{ +#if defined(LAUNCHER_APPLICATION) + + if (APPLICATION_DYN && m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() + && isOnline()) + { + m_manual_try++; + auto response = + CustomMessageBox::selectable(nullptr, + "Confirm retry", + "The tasks failed.\n" + "Failed urls\n" + + getFailedFiles().join("\n\t") + + ".\n" + "If this continues to happen please check the logs of the application.\n" + "Do you want to retry?", + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) + { + m_try = 0; + executeNextSubTask(); + return; + } + } +#endif + ConcurrentTask::emitFailed(reason); +} + +void NetJob::setAskRetry(bool askRetry) +{ + m_ask_retry = askRetry; +} diff --git a/archived/projt-launcher/launcher/net/NetJob.h b/archived/projt-launcher/launcher/net/NetJob.h new file mode 100644 index 0000000000..328363e604 --- /dev/null +++ b/archived/projt-launcher/launcher/net/NetJob.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include + +#include +#include "net/NetRequest.h" +#include "tasks/ConcurrentTask.h" + +// Those are included so that they are also included by anyone using NetJob +#include "net/Download.h" +#include "net/HttpMetaCache.h" + +class NetJob : public ConcurrentTask +{ + Q_OBJECT + + public: + using Ptr = shared_qobject_ptr; + + explicit NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent = -1); + ~NetJob() override = default; + + auto size() const -> int; + + auto canAbort() const -> bool override; + auto addNetAction(Net::NetRequest::Ptr action) -> bool; + + auto getFailedActions() -> QList; + auto getFailedFiles() -> QList; + void setAskRetry(bool askRetry); + + public slots: + // Qt can't handle auto at the start for some reason? + bool abort() override; + void emitFailed(QString reason) override; + + protected slots: + void executeNextSubTask() override; + + protected: + void updateState() override; + bool isOnline(); + + private: + shared_qobject_ptr m_network; + + int m_try = 1; + bool m_ask_retry = true; + int m_manual_try = 0; +}; diff --git a/archived/projt-launcher/launcher/net/NetRequest.cpp b/archived/projt-launcher/launcher/net/NetRequest.cpp new file mode 100644 index 0000000000..34aeac410e --- /dev/null +++ b/archived/projt-launcher/launcher/net/NetRequest.cpp @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "NetRequest.h" + +#include +#include +#include +#include +#include + +#if defined(LAUNCHER_APPLICATION) +#include "Application.h" +#endif +#include "BuildConfig.h" + +#include "MMCTime.h" +#include "StringUtils.h" + +namespace Net +{ + + void NetRequest::addValidator(Validator* v) + { + m_sink->addValidator(v); + } + + void NetRequest::executeTask() + { + setStatus(tr("Requesting %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80))); + + if (getState() == Task::State::AbortedByUser) + { + qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString(); + emit aborted(); + emit finished(); + return; + } + + QNetworkRequest request(m_url); + m_state = m_sink->init(request); + switch (m_state) + { + case State::Succeeded: + qCDebug(logCat) << getUid().toString() << "Request cache hit " << m_url.toString(); + emit succeeded(); + emit finished(); + return; + case State::Running: qCDebug(logCat) << getUid().toString() << "Running " << m_url.toString(); break; + case State::Inactive: + case State::Failed: + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); + emit finished(); + return; + case State::AbortedByUser: + emit aborted(); + emit finished(); + return; + } + +#if defined(LAUNCHER_APPLICATION) + auto user_agent = APPLICATION->getUserAgent(); +#else + auto user_agent = BuildConfig.USER_AGENT; +#endif + + request.setHeader(QNetworkRequest::UserAgentHeader, user_agent.toUtf8()); + // Force identity encoding to avoid Qt decompression issues on some platforms. + request.setRawHeader("Accept-Encoding", "identity"); + for (auto& header_proxy : m_headerProxies) + { + header_proxy->writeHeaders(request); + } + +#if defined(LAUNCHER_APPLICATION) + request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000); +#else + request.setTransferTimeout(); +#endif + + m_last_progress_time = m_clock.now(); + m_last_progress_bytes = 0; + + auto rep = getReply(request); + if (rep == nullptr) // it failed + return; + m_reply.reset(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &NetRequest::onProgress); + connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::onProgress); + connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); + connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError); + connect(rep, &QNetworkReply::sslErrors, this, &NetRequest::sslErrors); + connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); + } + + void NetRequest::onProgress(qint64 bytesReceived, qint64 bytesTotal) + { + auto now = m_clock.now(); + auto elapsed = now - m_last_progress_time; + + // use milliseconds for speed precision + auto elapsed_ms = std::chrono::duration_cast(elapsed); + auto bytes_received_since = bytesReceived - m_last_progress_bytes; + auto dl_speed_bps = (double)bytes_received_since / elapsed_ms.count() * 1000; + auto remaining_time_s = (bytesTotal - bytesReceived) / dl_speed_bps; + + //: Current amount of bytes downloaded, out of the total amount of bytes in the download + QString dl_progress = tr("%1 / %2") + .arg(StringUtils::humanReadableFileSize(bytesReceived)) + .arg(StringUtils::humanReadableFileSize(bytesTotal)); + + QString dl_speed_str; + if (elapsed_ms.count() > 0) + { + auto str_eta = bytesTotal > 0 ? Time::humanReadableDuration(remaining_time_s) : tr("unknown"); + //: Download speed, in bytes per second (remaining download time in parenthesis) + dl_speed_str = tr("%1 /s (%2)").arg(StringUtils::humanReadableFileSize(dl_speed_bps)).arg(str_eta); + } + else + { + //: Download speed at 0 bytes per second + dl_speed_str = tr("0 B/s"); + } + + setDetails(dl_progress + "\n" + dl_speed_str); + + setProgress(bytesReceived, bytesTotal); + } + + void NetRequest::downloadError(QNetworkReply::NetworkError error) + { + if (error == QNetworkReply::OperationCanceledError) + { + qCCritical(logCat) << getUid().toString() << "Aborted " << m_url.toString(); + m_state = State::Failed; + } + else + { + if (m_options & Option::AcceptLocalFiles) + { + if (m_sink->hasLocalData()) + { + m_state = State::Succeeded; + return; + } + } + // error happened during download. + qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with reason" << error; + if (m_reply) + qCCritical(logCat) << getUid().toString() << "HTTP Status" << replyStatusCode() << ";error" + << errorString(); + m_state = State::Failed; + } + } + + void NetRequest::sslErrors(const QList& errors) + { + int i = 1; + for (auto error : errors) + { + qCCritical(logCat) << getUid().toString() << "Request" << m_url.toString() << "SSL Error #" << i << " : " + << error.errorString(); + auto cert = error.certificate(); + qCCritical(logCat) << getUid().toString() << "Certificate in question:\n" << cert.toText(); + i++; + } + } + + auto NetRequest::handleRedirect() -> bool + { + QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); + if (!redirect.isValid()) + { + if (!m_reply->hasRawHeader("Location")) + { + // no redirect -> it's fine to continue + return false; + } + // there is a Location header, but it's not correct. we need to apply some workarounds... + QByteArray redirectBA = m_reply->rawHeader("Location"); + if (redirectBA.size() == 0) + { + // empty, yet present redirect header? WTF? + return false; + } + QString redirectStr = QString::fromUtf8(redirectBA); + + if (redirectStr.startsWith("//")) + { + /* + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 + */ + redirectStr = m_reply->url().scheme() + ":" + redirectStr; + } + else if (redirectStr.startsWith("/")) + { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); + } + + /* + * Parse in tolerant mode to recover malformed Location header values. + */ + redirect = QUrl(redirectStr, QUrl::TolerantMode); + if (!redirect.isValid()) + { + qCWarning(logCat) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; + downloadError(QNetworkReply::ProtocolFailure); + return false; + } + qCDebug(logCat) << getUid().toString() << "Fixed location header:" << redirect; + } + else + { + qCDebug(logCat) << getUid().toString() << "Location header:" << redirect; + } + + // Handle non-absolute redirects (for example "next" relative to the current path). + if (redirect.isRelative()) + { + redirect = m_reply->url().resolved(redirect); + } + + m_url = QUrl(redirect.toString()); + qCDebug(logCat) << getUid().toString() << "Following redirect to " << m_url.toString(); + executeTask(); + + return true; + } + + void NetRequest::downloadFinished() + { + // handle HTTP redirection first + if (handleRedirect()) + { + qCDebug(logCat) << getUid().toString() << "Request redirected:" << m_url.toString(); + return; + } + + // if the download failed before this point ... + if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) + { + qCDebug(logCat) << getUid().toString() + << "Request failed but we are allowed to proceed:" << m_url.toString(); + m_sink->abort(); + emit succeeded(); + emit finished(); + return; + } + else if (m_state == State::Failed) + { + qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); + m_sink->abort(); + m_failReason = m_reply->errorString(); + emit failed(m_reply->errorString()); + emit finished(); + return; + } + else if (m_state == State::AbortedByUser) + { + qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString(); + m_sink->abort(); + emit aborted(); + emit finished(); + return; + } + + // make sure we got all the remaining data, if any + auto data = m_reply->readAll(); + if (data.size()) + { + qCDebug(logCat) << getUid().toString() << "Writing extra" << data.size() << "bytes"; + m_state = m_sink->write(data); + if (m_state != State::Succeeded) + { + qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); + m_sink->abort(); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); + emit finished(); + return; + } + } + + // otherwise, finalize the whole graph + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) + { + qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); + m_sink->abort(); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); + emit finished(); + return; + } + + qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString(); + emit succeeded(); + emit finished(); + } + + void NetRequest::downloadReadyRead() + { + if (m_state == State::Running) + { + auto data = m_reply->readAll(); + m_state = m_sink->write(data); + if (m_state == State::Failed) + { + qCCritical(logCat) << getUid().toString() + << "Failed to process response chunk:" << m_sink->failReason(); + } + // qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes"; + } + else + { + qCCritical(logCat) << getUid().toString() << "Cannot write download data! illegal status " << m_status; + } + } + + auto NetRequest::abort() -> bool + { + m_state = State::AbortedByUser; + if (m_reply) + { + disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr); + m_reply->abort(); + } + return true; + } + + int NetRequest::replyStatusCode() const + { + return m_reply ? m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : -1; + } + + QNetworkReply::NetworkError NetRequest::error() const + { + return m_reply ? m_reply->error() : QNetworkReply::NoError; + } + + QUrl NetRequest::url() const + { + return m_url; + } + + QString NetRequest::errorString() const + { + return m_reply ? m_reply->errorString() : ""; + } +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/NetRequest.h b/archived/projt-launcher/launcher/net/NetRequest.h new file mode 100644 index 0000000000..8b5fd9a5bc --- /dev/null +++ b/archived/projt-launcher/launcher/net/NetRequest.h @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include + +#include "HeaderProxy.h" +#include "Sink.h" +#include "Validator.h" + +#include "QObjectPtr.h" +#include "net/Logging.h" +#include "tasks/Task.h" + +namespace Net +{ + class NetRequest : public Task + { + Q_OBJECT + protected: + explicit NetRequest() : Task() + {} + + public: + using Ptr = shared_qobject_ptr; + enum class Option + { + NoOptions = 0, + AcceptLocalFiles = 1, + MakeEternal = 2 + }; + Q_DECLARE_FLAGS(Options, Option) + + public: + ~NetRequest() override = default; + void addValidator(Validator* v); + auto abort() -> bool override; + auto canAbort() const -> bool override + { + return true; + } + + void setNetwork(shared_qobject_ptr network) + { + m_network = network; + } + void addHeaderProxy(Net::HeaderProxy* proxy) + { + m_headerProxies.push_back(std::shared_ptr(proxy)); + } + + QUrl url() const; + void setUrl(QUrl url) + { + m_url = url; + } + int replyStatusCode() const; + QNetworkReply::NetworkError error() const; + QString errorString() const; + + private: + auto handleRedirect() -> bool; + virtual QNetworkReply* getReply(QNetworkRequest&) = 0; + + protected slots: + void onProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadError(QNetworkReply::NetworkError error); + void sslErrors(const QList& errors); + void downloadFinished(); + void downloadReadyRead(); + void executeTask() override; + + protected: + std::unique_ptr m_sink; + Options m_options; + + using logCatFunc = const QLoggingCategory& (*)(); + logCatFunc logCat = taskUploadLogC; + + std::chrono::steady_clock m_clock; + std::chrono::time_point m_last_progress_time; + qint64 m_last_progress_bytes; + + shared_qobject_ptr m_network; + + /// the network reply + unique_qobject_ptr m_reply; + + /// source URL + QUrl m_url; + std::vector> m_headerProxies; + }; +} // namespace Net + +Q_DECLARE_OPERATORS_FOR_FLAGS(Net::NetRequest::Options) diff --git a/archived/projt-launcher/launcher/net/NetUtils.h b/archived/projt-launcher/launcher/net/NetUtils.h new file mode 100644 index 0000000000..f819504835 --- /dev/null +++ b/archived/projt-launcher/launcher/net/NetUtils.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include +#include + +namespace Net +{ + inline bool isApplicationError(QNetworkReply::NetworkError x) + { + // Mainly taken from https://github.com/qt/qtbase/blob/dev/src/network/access/qhttpthreaddelegate.cpp + static QSet errors = { QNetworkReply::ProtocolInvalidOperationError, + QNetworkReply::AuthenticationRequiredError, + QNetworkReply::ContentAccessDenied, + QNetworkReply::ContentNotFoundError, + QNetworkReply::ContentOperationNotPermittedError, + QNetworkReply::ProxyAuthenticationRequiredError, + QNetworkReply::ContentConflictError, + QNetworkReply::ContentGoneError, + QNetworkReply::InternalServerError, + QNetworkReply::OperationNotImplementedError, + QNetworkReply::ServiceUnavailableError, + QNetworkReply::UnknownServerError, + QNetworkReply::UnknownContentError }; + return errors.contains(x); + } +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/PasteUpload.cpp b/archived/projt-launcher/launcher/net/PasteUpload.cpp new file mode 100644 index 0000000000..796d4e2ee7 --- /dev/null +++ b/archived/projt-launcher/launcher/net/PasteUpload.cpp @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * Copyright (C) 2022 Swirl + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ +#include "PasteUpload.h" +#include + +#include +#include +#include +#include +#include +#include +#include "logs/LogRedactor.hpp" + +const std::array PasteUpload::PasteTypes = { + { { "0x0.st", "https://0x0.st", "" }, + { "hastebin", "https://hst.sh", "/documents" }, + { "paste.gg", "https://paste.gg", "/api/v1/pastes" }, + { "mclo.gs", "https://api.mclo.gs", "/1/log" } } +}; + +QNetworkReply* PasteUpload::getReply(QNetworkRequest& request) +{ + switch (m_paste_type) + { + case PasteUpload::NullPointer: + { + QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this }; + + QHttpPart filePart; + filePart.setBody(m_log.toUtf8()); + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + "form-data; name=\"file\"; filename=\"log.txt\""); + multiPart->append(filePart); + + return m_network->post(request, multiPart); + } + case PasteUpload::Hastebin: + { + return m_network->post(request, m_log.toUtf8()); + } + case PasteUpload::Mclogs: + { + QUrlQuery postData; + postData.addQueryItem("content", m_log); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + return m_network->post(request, postData.toString().toUtf8()); + } + case PasteUpload::PasteGG: + { + QJsonObject obj; + QJsonDocument doc; + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + obj.insert("expires", QDateTime::currentDateTimeUtc().addDays(100).toString(Qt::DateFormat::ISODate)); + + QJsonArray files; + QJsonObject logFileInfo; + QJsonObject logFileContentInfo; + logFileContentInfo.insert("format", "text"); + logFileContentInfo.insert("value", m_log); + logFileInfo.insert("name", "log.txt"); + logFileInfo.insert("content", logFileContentInfo); + files.append(logFileInfo); + + obj.insert("files", files); + + doc.setObject(obj); + return m_network->post(request, doc.toJson()); + } + } + + return nullptr; +}; + +auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State +{ + if (!finalizeAllValidators(reply)) + { + m_fail_reason = "Failed to finalize validators"; + return Task::State::Failed; + } + int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (reply.error() != QNetworkReply::NetworkError::NoError) + { + m_fail_reason = QObject::tr("Network error: %1").arg(reply.errorString()); + return Task::State::Failed; + } + else if (statusCode != 200 && statusCode != 201) + { + QString reasonPhrase = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + m_fail_reason = QObject::tr("Error: %1 returned unexpected status code %2 %3") + .arg(m_d->url().toString()) + .arg(statusCode) + .arg(reasonPhrase); + return Task::State::Failed; + } + + switch (m_d->m_paste_type) + { + case PasteUpload::NullPointer: m_d->m_pasteLink = QString::fromUtf8(*m_output).trimmed(); break; + case PasteUpload::Hastebin: + { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = QObject::tr("Failed to parse response from hastebin server: expected JSON but got an " + "invalid response. Error: %1") + .arg(jsonError.errorString()); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("key") && obj["key"].isString()) + { + QString key = doc.object()["key"].toString(); + m_d->m_pasteLink = m_d->m_baseUrl + "/" + key; + } + else + { + qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); + return Task::State::Failed; + } + break; + } + case PasteUpload::Mclogs: + { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = QObject::tr("Failed to parse response from mclogs server: expected JSON but got an " + "invalid response. Error: %1") + .arg(jsonError.errorString()); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("success") && obj["success"].isBool()) + { + bool success = obj["success"].toBool(); + if (success) + { + m_d->m_pasteLink = obj["url"].toString(); + } + else + { + QString error = obj["error"].toString(); + m_fail_reason = QObject::tr("Error: %1 returned an error: %2").arg(m_d->url().toString(), error); + return Task::State::Failed; + } + } + else + { + qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); + return Task::State::Failed; + } + break; + } + case PasteUpload::PasteGG: + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = QObject::tr("Failed to parse response from pasteGG server: expected JSON but got an " + "invalid response. Error: %1") + .arg(jsonError.errorString()); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("status") && obj["status"].isString()) + { + QString status = obj["status"].toString(); + if (status == "success") + { + m_d->m_pasteLink = m_d->m_baseUrl + "/p/anonymous/" + obj["result"].toObject()["id"].toString(); + } + else + { + QString error = obj["error"].toString(); + QString message = + (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; + m_fail_reason = QObject::tr("Error: %1 returned an error code: %2\nError message: %3") + .arg(m_d->url().toString(), error, message); + return Task::State::Failed; + } + } + else + { + qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); + return Task::State::Failed; + } + break; + } + return Task::State::Succeeded; +} + +PasteUpload::PasteUpload(const QString& log, QString url, PasteType pasteType) + : m_log(log), + m_baseUrl(url), + m_paste_type(pasteType) +{ + projt::logs::redactLog(m_log); + auto base = PasteUpload::PasteTypes.at(pasteType); + if (m_baseUrl.isEmpty()) + m_baseUrl = base.defaultBase; + + // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't + // follow that?? + if (pasteType == PasteUpload::PasteGG && m_baseUrl == base.defaultBase) + m_url = "https://api.paste.gg/v1/pastes"; + else + m_url = m_baseUrl + base.endpointPath; + + m_sink.reset(new Sink(this)); +} diff --git a/archived/projt-launcher/launcher/net/PasteUpload.h b/archived/projt-launcher/launcher/net/PasteUpload.h new file mode 100644 index 0000000000..c60774e2aa --- /dev/null +++ b/archived/projt-launcher/launcher/net/PasteUpload.h @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "net/ByteArraySink.h" +#include "net/NetRequest.h" +#include "tasks/Task.h" + +#include +#include +#include + +#include +#include + +class PasteUpload : public Net::NetRequest +{ + public: + enum PasteType : int + { + // 0x0.st + NullPointer, + // hastebin.com + Hastebin, + // paste.gg + PasteGG, + // mclo.gs + Mclogs, + // Helpful to get the range of valid values on the enum for input sanitisation: + First = NullPointer, + Last = Mclogs + }; + struct PasteTypeInfo + { + const QString name; + const QString defaultBase; + const QString endpointPath; + }; + + static const std::array PasteTypes; + + class Sink : public Net::ByteArraySink + { + public: + Sink(PasteUpload* p) : Net::ByteArraySink(std::make_shared()), m_d(p) {}; + virtual ~Sink() = default; + + public: + auto finalize(QNetworkReply& reply) -> Task::State override; + + private: + PasteUpload* m_d; + }; + friend Sink; + + PasteUpload(const QString& log, QString url, PasteType pasteType); + virtual ~PasteUpload() = default; + + QString pasteLink() + { + return m_pasteLink; + } + + private: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + QString m_log; + QString m_pasteLink; + QString m_baseUrl; + const PasteType m_paste_type; +}; diff --git a/archived/projt-launcher/launcher/net/RawHeaderProxy.h b/archived/projt-launcher/launcher/net/RawHeaderProxy.h new file mode 100644 index 0000000000..19df14337b --- /dev/null +++ b/archived/projt-launcher/launcher/net/RawHeaderProxy.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "net/HeaderProxy.h" + +namespace Net +{ + + class RawHeaderProxy : public HeaderProxy + { + public: + RawHeaderProxy(QList headers = {}) : HeaderProxy(), m_headers(std::move(headers)) {}; + virtual ~RawHeaderProxy() = default; + + public: + virtual QList headers(const QNetworkRequest&) const override + { + return m_headers; + }; + + void addHeader(const HeaderPair& header) + { + m_headers.append(header); + } + void addHeader(const QByteArray& headerName, const QByteArray& headerValue) + { + m_headers.append({ headerName, headerValue }); + } + void addHeaders(const QList& headers) + { + m_headers.append(headers); + } + void setHeaders(QList headers) + { + m_headers = headers; + }; + + private: + QList m_headers; + }; + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/Sink.h b/archived/projt-launcher/launcher/net/Sink.h new file mode 100644 index 0000000000..bfa64a9bda --- /dev/null +++ b/archived/projt-launcher/launcher/net/Sink.h @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "Validator.h" +#include "tasks/Task.h" + +namespace Net +{ + class Sink + { + public: + Sink() = default; + virtual ~Sink() = default; + + public: + virtual auto init(QNetworkRequest& request) -> Task::State = 0; + virtual auto write(QByteArray& data) -> Task::State = 0; + virtual auto abort() -> Task::State = 0; + virtual auto finalize(QNetworkReply& reply) -> Task::State = 0; + + virtual auto hasLocalData() -> bool = 0; + + QString failReason() const + { + return m_fail_reason; + } + + void addValidator(Validator* validator) + { + if (validator) + { + validators.push_back(std::shared_ptr(validator)); + } + } + + protected: + bool initAllValidators(QNetworkRequest& request) + { + for (auto& validator : validators) + { + if (!validator->init(request)) + return false; + } + return true; + } + bool finalizeAllValidators(QNetworkReply& reply) + { + for (auto& validator : validators) + { + if (!validator->validate(reply)) + return false; + } + return true; + } + bool failAllValidators() + { + bool success = true; + for (auto& validator : validators) + { + success &= validator->abort(); + } + return success; + } + bool writeAllValidators(QByteArray& data) + { + for (auto& validator : validators) + { + if (!validator->write(data)) + return false; + } + return true; + } + + protected: + std::vector> validators; + QString m_fail_reason; + }; +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/Upload.cpp b/archived/projt-launcher/launcher/net/Upload.cpp new file mode 100644 index 0000000000..58bd36bf25 --- /dev/null +++ b/archived/projt-launcher/launcher/net/Upload.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "Upload.h" + +#include +#include +#include "ByteArraySink.h" + +namespace Net +{ + + QNetworkReply* Upload::getReply(QNetworkRequest& request) + { + if (!request.hasRawHeader("Content-Type")) + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + return m_network->post(request, m_post_data); + } + + Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) + { + auto up = makeShared(); + up->m_url = std::move(url); + up->m_sink.reset(new ByteArraySink(output)); + up->m_post_data = std::move(m_post_data); + return up; + } +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/Upload.h b/archived/projt-launcher/launcher/net/Upload.h new file mode 100644 index 0000000000..0f4e001ead --- /dev/null +++ b/archived/projt-launcher/launcher/net/Upload.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetRequest.h" + +namespace Net +{ + + class Upload : public NetRequest + { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr; + explicit Upload() : NetRequest() + { + logCat = taskUploadLogC; + }; + + static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + QByteArray m_post_data; + }; + +} // namespace Net diff --git a/archived/projt-launcher/launcher/net/Validator.h b/archived/projt-launcher/launcher/net/Validator.h new file mode 100644 index 0000000000..59c9032922 --- /dev/null +++ b/archived/projt-launcher/launcher/net/Validator.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#pragma once + +#include + +namespace Net +{ + class Validator + { + public: /* con/des */ + Validator() + {} + virtual ~Validator() + {} + + public: /* methods */ + virtual bool init(QNetworkRequest& request) = 0; + virtual bool write(QByteArray& data) = 0; + virtual bool abort() = 0; + virtual bool validate(QNetworkReply& reply) = 0; + }; +} // namespace Net diff --git a/archived/projt-launcher/launcher/news/NewsChecker.cpp b/archived/projt-launcher/launcher/news/NewsChecker.cpp new file mode 100644 index 0000000000..e3c2ba41a4 --- /dev/null +++ b/archived/projt-launcher/launcher/news/NewsChecker.cpp @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * + * + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ======================================================================== */ + +#include "NewsChecker.h" + +#include +#include + +#include + +NewsChecker::NewsChecker(shared_qobject_ptr network, const QString& feedUrl) +{ + m_network = network; + m_feedUrl = feedUrl; +} + +void NewsChecker::reloadNews() +{ + // Start a netjob to download the RSS feed and call rssDownloadFinished() when it's done. + if (isLoadingNews()) + { + qDebug() << "Ignored request to reload news. Currently reloading already."; + return; + } + + qDebug() << "Reloading news."; + + NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; + job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); + job->setAskRetry(false); + connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); + connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); + m_newsNetJob.reset(job); + job->start(); +} + +void NewsChecker::rssDownloadFinished() +{ + // Parse the XML file and process the RSS feed entries. + qDebug() << "Finished loading RSS feed."; + + m_newsNetJob.reset(); + QDomDocument doc; + { + // Stuff to store error info in. + QString errorMsg = "Unknown error."; + int errorLine = -1; + int errorCol = -1; + + // Parse the XML. + if (!doc.setContent(*newsData, false, &errorMsg, &errorLine, &errorCol)) + { + QString fullErrorMsg = + QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol); + fail(fullErrorMsg); + newsData->clear(); + return; + } + newsData->clear(); + } + + // If the parsing succeeded, read it. + QDomNodeList items = doc.elementsByTagName("entry"); + if (items.isEmpty()) + { + items = doc.elementsByTagName("item"); + } + + m_newsEntries.clear(); + for (int i = 0; i < items.length(); i++) + { + QDomElement element = items.at(i).toElement(); + NewsEntryPtr entry; + entry.reset(new NewsEntry()); + QString errorMsg = "An unknown error occurred."; + if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) + { + qDebug() << "Loaded news entry" << entry->title; + m_newsEntries.append(entry); + } + else + { + qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg; + } + } + + succeed(); +} + +void NewsChecker::rssDownloadFailed(QString reason) +{ + // Set an error message and fail. + fail(tr("Failed to load news RSS feed:\n%1").arg(reason)); +} + +QList NewsChecker::getNewsEntries() const +{ + return m_newsEntries; +} + +bool NewsChecker::isLoadingNews() const +{ + return m_newsNetJob.get() != nullptr; +} + +QString NewsChecker::getLastLoadErrorMsg() const +{ + return m_lastLoadError; +} + +void NewsChecker::succeed() +{ + m_lastLoadError = ""; + qDebug() << "News loading succeeded."; + m_newsNetJob.reset(); + emit newsLoaded(); +} + +void NewsChecker::fail(const QString& errorMsg) +{ + m_lastLoadError = errorMsg; + qDebug() << "Failed to load news:" << errorMsg; + m_newsNetJob.reset(); + emit newsLoadingFailed(errorMsg); +} diff --git a/archived/projt-launcher/launcher/news/NewsChecker.h b/archived/projt-launcher/launcher/news/NewsChecker.h new file mode 100644 index 0000000000..c9fab506ad --- /dev/null +++ b/archived/projt-launcher/launcher/news/NewsChecker.h @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include +#include + +#include + +#include "NewsEntry.h" + +class NewsChecker : public QObject +{ + Q_OBJECT + public: + /*! + * Constructs a news reader to read from the given RSS feed URL. + */ + NewsChecker(shared_qobject_ptr network, const QString& feedUrl); + + /*! + * Returns the error message for the last time the news was loaded. + * Empty string if the last load was successful. + */ + QString getLastLoadErrorMsg() const; + + /*! + * Returns true if the news has been loaded successfully. + */ + bool isNewsLoaded() const; + + //! True if the news is currently loading. If true, reloadNews() will do nothing. + bool isLoadingNews() const; + + /*! + * Returns a list of news entries. + */ + QList getNewsEntries() const; + + /*! + * Reloads the news from the website's RSS feed. + * If the news is already loading, this does nothing. + */ + void Q_SLOT reloadNews(); + + signals: + /*! + * Signal fired after the news has finished loading. + */ + void newsLoaded(); + + /*! + * Signal fired after the news fails to load. + */ + void newsLoadingFailed(QString errorMsg); + + protected slots: + void rssDownloadFinished(); + void rssDownloadFailed(QString reason); + + protected: /* data */ + //! The URL for the RSS feed to fetch. + QString m_feedUrl; + + //! List of news entries. + QList m_newsEntries; + + //! The network job to use to load the news. + NetJob::Ptr m_newsNetJob; + + //! True if news has been loaded. + bool m_loadedNews; + + std::shared_ptr newsData = std::make_shared(); + + /*! + * Gets the error message that was given last time the news was loaded. + * If the last news load succeeded, this will be an empty string. + */ + QString m_lastLoadError; + + shared_qobject_ptr m_network; + + protected slots: + /// Emits newsLoaded() and sets m_lastLoadError to empty string. + void succeed(); + + /// Emits newsLoadingFailed() and sets m_lastLoadError to the given message. + void fail(const QString& errorMsg); +}; diff --git a/archived/projt-launcher/launcher/news/NewsEntry.cpp b/archived/projt-launcher/launcher/news/NewsEntry.cpp new file mode 100644 index 0000000000..b2eb79ff9b --- /dev/null +++ b/archived/projt-launcher/launcher/news/NewsEntry.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#include "NewsEntry.h" + +#include +#include + +NewsEntry::NewsEntry(QObject* parent) : QObject(parent) +{ + this->title = tr("Untitled"); + this->content = tr("No content."); + this->link = ""; +} + +NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent) + : QObject(parent) +{ + this->title = title; + this->content = content; + this->link = link; +} + +/*! + * Gets the text content of the given child element as a QVariant. + */ +inline QString childValue(const QDomElement& element, const QString& childName, QString defaultVal = "") +{ + QDomNodeList nodes = element.elementsByTagName(childName); + if (nodes.count() > 0) + { + QDomElement elem = nodes.at(0).toElement(); + return elem.text(); + } + else + { + return defaultVal; + } +} + +bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, [[maybe_unused]] QString* errorMsg) +{ + QString title = childValue(element, "title", tr("Untitled")); + QString content = childValue(element, "content", QString()); + if (content.isEmpty()) + content = childValue(element, "content:encoded", QString()); + if (content.isEmpty()) + content = childValue(element, "description", QString()); + if (content.isEmpty()) + content = childValue(element, "summary", tr("No content.")); + + QString link = childValue(element, "id", QString()); + if (link.isEmpty()) + link = childValue(element, "link", QString()); + if (link.isEmpty()) + { + QDomNodeList linkNodes = element.elementsByTagName("link"); + if (linkNodes.count() > 0) + { + QDomElement linkElem = linkNodes.at(0).toElement(); + link = linkElem.attribute("href"); + } + } + + entry->title = title; + entry->content = content; + entry->link = link; + return true; +} diff --git a/archived/projt-launcher/launcher/news/NewsEntry.h b/archived/projt-launcher/launcher/news/NewsEntry.h new file mode 100644 index 0000000000..bfde41bbe4 --- /dev/null +++ b/archived/projt-launcher/launcher/news/NewsEntry.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * === Upstream License Block (Do Not Modify) ============================== + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================================== */ + +#pragma once + +#include +#include +#include +#include + +class NewsEntry : public QObject +{ + Q_OBJECT + + public: + /*! + * Constructs an empty news entry. + */ + explicit NewsEntry(QObject* parent = 0); + + /*! + * Constructs a new news entry. + * Note that content may contain HTML. + */ + NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent = 0); + + /*! + * Attempts to load information from the given XML element into the given news entry pointer. + * If this fails, the function will return false and store an error message in the errorMsg pointer. + */ + static bool fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg = 0); + + //! The post title. + QString title; + + //! The post's content. May contain HTML. + QString content; + + //! URL to the post. + QString link; +}; + +using NewsEntryPtr = std::shared_ptr; diff --git a/archived/projt-launcher/launcher/qtlogging.ini b/archived/projt-launcher/launcher/qtlogging.ini new file mode 100644 index 0000000000..10f724163e --- /dev/null +++ b/archived/projt-launcher/launcher/qtlogging.ini @@ -0,0 +1,19 @@ +[Rules] +*.debug=true +# prevent log spam and strange bugs +# qt.qpa.drawing in particular causes theme artifacts on MacOS +qt.*.debug=false +# supress image format noise +kf.imageformats.plugins.hdr=false +kf.imageformats.plugins.xcf=false +# don't log credentials by default +launcher.auth.credentials.debug=false +# remove the debug lines, other log levels still get through +launcher.task.net.download.debug=false +# enable or disable whole catageries +launcher.task.net=true +launcher.task=false +launcher.task.net.upload=true +launcher.task.net.metacache=false +launcher.task.net.metacache.http=true + diff --git a/archived/projt-launcher/launcher/resources/OSX/OSX.qrc b/archived/projt-launcher/launcher/resources/OSX/OSX.qrc new file mode 100644 index 0000000000..49f56b0c14 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/OSX.qrc @@ -0,0 +1,43 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/tag.svg + scalable/export.svg + scalable/rename.svg + scalable/launch.svg + scalable/shortcut.svg + + diff --git a/archived/projt-launcher/launcher/resources/OSX/index.theme b/archived/projt-launcher/launcher/resources/OSX/index.theme new file mode 100644 index 0000000000..7f90a32ed2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=OSX +Comment=OSX theme by pexner +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/about.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/about.svg new file mode 100644 index 0000000000..eb87ccf183 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/about.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/accounts.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/accounts.svg new file mode 100644 index 0000000000..163bcee011 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/accounts.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/bug.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/bug.svg new file mode 100644 index 0000000000..00565bb642 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/bug.svg @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/centralmods.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/centralmods.svg new file mode 100644 index 0000000000..37b821e46b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/centralmods.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/checkupdate.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/checkupdate.svg new file mode 100644 index 0000000000..30cec51f18 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/checkupdate.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/copy.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/copy.svg new file mode 100644 index 0000000000..7382d6e267 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/copy.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/coremods.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/coremods.svg new file mode 100644 index 0000000000..b0df60529d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/coremods.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/custom-commands.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/custom-commands.svg new file mode 100644 index 0000000000..e663452b6f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/custom-commands.svg @@ -0,0 +1,71 @@ + +image/svg+xml diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/delete.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/delete.svg new file mode 100644 index 0000000000..bec8c7d97f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/delete.svg @@ -0,0 +1,49 @@ + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/export.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/export.svg new file mode 100644 index 0000000000..62145a7e65 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/export.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/externaltools.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/externaltools.svg new file mode 100644 index 0000000000..a2b7488e1a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/externaltools.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/help.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/help.svg new file mode 100644 index 0000000000..9d1b367c8e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/help.svg @@ -0,0 +1,51 @@ + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/instance-settings.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/instance-settings.svg new file mode 100644 index 0000000000..394877f81e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/instance-settings.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/jarmods.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/jarmods.svg new file mode 100644 index 0000000000..213ec83346 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/jarmods.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/java.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/java.svg new file mode 100644 index 0000000000..e1aee15928 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/java.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/language.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/language.svg new file mode 100644 index 0000000000..4f7d002a57 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/language.svg @@ -0,0 +1,40 @@ + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/launch.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/launch.svg new file mode 100644 index 0000000000..fb18916252 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/launch.svg @@ -0,0 +1,33 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/loadermods.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/loadermods.svg new file mode 100644 index 0000000000..76951ebd73 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/loadermods.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/log.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/log.svg new file mode 100644 index 0000000000..0ac45d54fd --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/log.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/minecraft.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/minecraft.svg new file mode 100644 index 0000000000..86c915bc98 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/minecraft.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/new.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/new.svg new file mode 100644 index 0000000000..79ee87ba9b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/new.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/news.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/news.svg new file mode 100644 index 0000000000..b8ce3cd117 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/news.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/notes.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/notes.svg new file mode 100644 index 0000000000..c2e95cfd9f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/notes.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/patreon.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/patreon.svg new file mode 100644 index 0000000000..4f0da3e56b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/patreon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/proxy.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/proxy.svg new file mode 100644 index 0000000000..99acaa2b70 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/proxy.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/refresh.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/refresh.svg new file mode 100644 index 0000000000..c97489c1ab --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/refresh.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/rename.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/rename.svg new file mode 100644 index 0000000000..83ae5cb55b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/rename.svg @@ -0,0 +1,27 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/resourcepacks.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/resourcepacks.svg new file mode 100644 index 0000000000..c85d4e3ce6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/resourcepacks.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/screenshots.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/screenshots.svg new file mode 100644 index 0000000000..12df0c883e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/screenshots.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/settings.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/settings.svg new file mode 100644 index 0000000000..dcdd9f1c2a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/settings.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/shaderpacks.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/shaderpacks.svg new file mode 100644 index 0000000000..cf8251bacc --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/shaderpacks.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/shortcut.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/shortcut.svg new file mode 100644 index 0000000000..a2b7488e1a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/shortcut.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/status-bad.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/status-bad.svg new file mode 100644 index 0000000000..add7a6f7d0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/status-bad.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/status-good.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/status-good.svg new file mode 100644 index 0000000000..f10da757bb --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/status-good.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/status-yellow.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/status-yellow.svg new file mode 100644 index 0000000000..fba697bcd0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/status-yellow.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/tag.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/tag.svg new file mode 100644 index 0000000000..56438e3b57 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/tag.svg @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/viewfolder.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/viewfolder.svg new file mode 100644 index 0000000000..682c72c7d9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/viewfolder.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/OSX/scalable/worlds.svg b/archived/projt-launcher/launcher/resources/OSX/scalable/worlds.svg new file mode 100644 index 0000000000..b14912720a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/OSX/scalable/worlds.svg @@ -0,0 +1,58 @@ + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/assets/underconstruction.png b/archived/projt-launcher/launcher/resources/assets/underconstruction.png new file mode 100644 index 0000000000..5f2fdf9e45 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/assets/underconstruction.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/backgrounds.qrc b/archived/projt-launcher/launcher/resources/backgrounds/backgrounds.qrc new file mode 100644 index 0000000000..e63a25b5a1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/backgrounds/backgrounds.qrc @@ -0,0 +1,29 @@ + + + + kitteh.png + kitteh-xmas.png + kitteh-bday.png + kitteh-spooky.png + rory.png + rory-xmas.png + rory-bday.png + rory-spooky.png + rory-flat.png + rory-flat-xmas.png + rory-flat-bday.png + rory-flat-spooky.png + + + + + teawie.png + + teawie-xmas.png + + teawie-bday.png + + teawie-spooky.png + + + diff --git a/archived/projt-launcher/launcher/resources/backgrounds/kitteh-bday.png b/archived/projt-launcher/launcher/resources/backgrounds/kitteh-bday.png new file mode 100644 index 0000000000..f4a7bbc1fe Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/kitteh-bday.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/kitteh-spooky.png b/archived/projt-launcher/launcher/resources/backgrounds/kitteh-spooky.png new file mode 100644 index 0000000000..bb3765f92f Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/kitteh-spooky.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/kitteh-xmas.png b/archived/projt-launcher/launcher/resources/backgrounds/kitteh-xmas.png new file mode 100644 index 0000000000..1e92e90817 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/kitteh-xmas.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/kitteh.png b/archived/projt-launcher/launcher/resources/backgrounds/kitteh.png new file mode 100644 index 0000000000..fa3d525488 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/kitteh.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory-bday.png b/archived/projt-launcher/launcher/resources/backgrounds/rory-bday.png new file mode 100644 index 0000000000..8c796927cc Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory-bday.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-bday.png b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-bday.png new file mode 100644 index 0000000000..94c4509a44 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-bday.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-spooky.png b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-spooky.png new file mode 100644 index 0000000000..4a0046c2b8 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-spooky.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-xmas.png b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-xmas.png new file mode 100644 index 0000000000..e6278ed5c2 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat-xmas.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory-flat.png b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat.png new file mode 100644 index 0000000000..22fe618870 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory-flat.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory-spooky.png b/archived/projt-launcher/launcher/resources/backgrounds/rory-spooky.png new file mode 100644 index 0000000000..1aa9286715 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory-spooky.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory-xmas.png b/archived/projt-launcher/launcher/resources/backgrounds/rory-xmas.png new file mode 100644 index 0000000000..f33e92666b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory-xmas.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/rory.png b/archived/projt-launcher/launcher/resources/backgrounds/rory.png new file mode 100644 index 0000000000..5570499c22 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/rory.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/teawie-bday.png b/archived/projt-launcher/launcher/resources/backgrounds/teawie-bday.png new file mode 100644 index 0000000000..b4621f9b5c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/teawie-bday.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/teawie-spooky.png b/archived/projt-launcher/launcher/resources/backgrounds/teawie-spooky.png new file mode 100644 index 0000000000..194d8ab7ce Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/teawie-spooky.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/teawie-xmas.png b/archived/projt-launcher/launcher/resources/backgrounds/teawie-xmas.png new file mode 100644 index 0000000000..54a09ae51c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/teawie-xmas.png differ diff --git a/archived/projt-launcher/launcher/resources/backgrounds/teawie.png b/archived/projt-launcher/launcher/resources/backgrounds/teawie.png new file mode 100644 index 0000000000..99b60ad1ec Binary files /dev/null and b/archived/projt-launcher/launcher/resources/backgrounds/teawie.png differ diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/breeze_dark.qrc b/archived/projt-launcher/launcher/resources/breeze_dark/breeze_dark.qrc new file mode 100644 index 0000000000..585f2c60a5 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/breeze_dark.qrc @@ -0,0 +1,48 @@ + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/datapacks.svg + scalable/discord.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/matrix.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/proxy.svg + scalable/reddit-alien.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/shortcut.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/tag.svg + scalable/export.svg + scalable/rename.svg + scalable/launch.svg + scalable/server.svg + scalable/appearance.svg + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/index.theme b/archived/projt-launcher/launcher/resources/breeze_dark/index.theme new file mode 100644 index 0000000000..f9f6f4dc0c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=Breeze Dark +Comment=Breeze Dark Icons +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/about.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/about.svg new file mode 100644 index 0000000000..856d1b2b8b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/about.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/accounts.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/accounts.svg new file mode 100644 index 0000000000..fbb5195929 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/accounts.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/appearance.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/appearance.svg new file mode 100644 index 0000000000..93e6ffa764 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/appearance.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/bug.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/bug.svg new file mode 100644 index 0000000000..6ddf482f7b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/bug.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/centralmods.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/centralmods.svg new file mode 100644 index 0000000000..4035e51cb4 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/centralmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/checkupdate.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/checkupdate.svg new file mode 100644 index 0000000000..cc5dfc1635 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/checkupdate.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/copy.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/copy.svg new file mode 100644 index 0000000000..fe4a36acd7 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/copy.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/coremods.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/coremods.svg new file mode 100644 index 0000000000..ec4ecea854 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/coremods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/custom-commands.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/custom-commands.svg new file mode 100644 index 0000000000..44efd39ef9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/custom-commands.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/datapacks.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/datapacks.svg new file mode 100644 index 0000000000..308c4a239d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/datapacks.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/delete.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/delete.svg new file mode 100644 index 0000000000..c7074585b6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/delete.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/discord.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/discord.svg new file mode 100644 index 0000000000..2e6d889990 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/export.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/export.svg new file mode 100644 index 0000000000..b1fe39d14c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/export.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/externaltools.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/externaltools.svg new file mode 100644 index 0000000000..dd19fb90f3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/externaltools.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/help.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/help.svg new file mode 100644 index 0000000000..b273a8bcf9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/help.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/instance-settings.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/instance-settings.svg new file mode 100644 index 0000000000..c5f0504b60 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/instance-settings.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/jarmods.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/jarmods.svg new file mode 100644 index 0000000000..49a45d36ad --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/jarmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/java.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/java.svg new file mode 100644 index 0000000000..7149981cc4 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/java.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/language.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/language.svg new file mode 100644 index 0000000000..239cdf94e1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/language.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/launch.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/launch.svg new file mode 100644 index 0000000000..25c5fabc00 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/launch.svg @@ -0,0 +1,8 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/loadermods.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/loadermods.svg new file mode 100644 index 0000000000..7bd8718822 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/loadermods.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/log.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/log.svg new file mode 100644 index 0000000000..fcd83c4d83 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/log.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/matrix.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/matrix.svg new file mode 100644 index 0000000000..214f57080d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/matrix.svg @@ -0,0 +1,9 @@ + + + Matrix (protocol) logo + + + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/minecraft.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/minecraft.svg new file mode 100644 index 0000000000..1d8d01675b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/minecraft.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/new.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/new.svg new file mode 100644 index 0000000000..316017273c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/new.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/news.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/news.svg new file mode 100644 index 0000000000..a2ff0c8d18 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/news.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/notes.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/notes.svg new file mode 100644 index 0000000000..6452d3c8d6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/notes.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/patreon.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/patreon.svg new file mode 100644 index 0000000000..8886128830 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/patreon.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/proxy.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/proxy.svg new file mode 100644 index 0000000000..c6efb17162 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/proxy.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/reddit-alien.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/reddit-alien.svg new file mode 100644 index 0000000000..90acfc591e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/reddit-alien.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/refresh.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/refresh.svg new file mode 100644 index 0000000000..7b48646399 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/refresh.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/rename.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/rename.svg new file mode 100644 index 0000000000..6a844965ea --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/rename.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/resourcepacks.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/resourcepacks.svg new file mode 100644 index 0000000000..0986c2167d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/resourcepacks.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/screenshots.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/screenshots.svg new file mode 100644 index 0000000000..a10ed713d4 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/screenshots.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/server.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/server.svg new file mode 100644 index 0000000000..7d9af3e714 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/server.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/settings.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/settings.svg new file mode 100644 index 0000000000..009d81547e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/settings.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/shaderpacks.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/shaderpacks.svg new file mode 100644 index 0000000000..b2887947a3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/shaderpacks.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/shortcut.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/shortcut.svg new file mode 100644 index 0000000000..5559be1df0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/shortcut.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-bad.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-bad.svg new file mode 100644 index 0000000000..6fc3137e48 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-bad.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-good.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-good.svg new file mode 100644 index 0000000000..eb8bc03be4 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-good.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-yellow.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-yellow.svg new file mode 100644 index 0000000000..1dc4d0f51b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/status-yellow.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/tag.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/tag.svg new file mode 100644 index 0000000000..b54b515fc8 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/tag.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/viewfolder.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/viewfolder.svg new file mode 100644 index 0000000000..0189b9544b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/viewfolder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_dark/scalable/worlds.svg b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/worlds.svg new file mode 100644 index 0000000000..0cff82666e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_dark/scalable/worlds.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/breeze_light.qrc b/archived/projt-launcher/launcher/resources/breeze_light/breeze_light.qrc new file mode 100644 index 0000000000..2b0adba10a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/breeze_light.qrc @@ -0,0 +1,48 @@ + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/datapacks.svg + scalable/discord.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/matrix.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/proxy.svg + scalable/reddit-alien.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/shortcut.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/tag.svg + scalable/export.svg + scalable/rename.svg + scalable/launch.svg + scalable/server.svg + scalable/appearance.svg + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/index.theme b/archived/projt-launcher/launcher/resources/breeze_light/index.theme new file mode 100644 index 0000000000..126d42d736 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=Breeze Light +Comment=Breeze Light Icons +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/about.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/about.svg new file mode 100644 index 0000000000..ea1dc02cdb --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/about.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/accounts.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/accounts.svg new file mode 100644 index 0000000000..8a542f369d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/accounts.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/appearance.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/appearance.svg new file mode 100644 index 0000000000..6e6d64a792 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/appearance.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/bug.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/bug.svg new file mode 100644 index 0000000000..4f41ad6b85 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/bug.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/centralmods.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/centralmods.svg new file mode 100644 index 0000000000..174206c432 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/centralmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/checkupdate.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/checkupdate.svg new file mode 100644 index 0000000000..06b318273a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/checkupdate.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/copy.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/copy.svg new file mode 100644 index 0000000000..2557953b04 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/copy.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/coremods.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/coremods.svg new file mode 100644 index 0000000000..e4615cfa7a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/coremods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/custom-commands.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/custom-commands.svg new file mode 100644 index 0000000000..b2ac78c59c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/custom-commands.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/datapacks.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/datapacks.svg new file mode 100644 index 0000000000..f5d4acc3bb --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/datapacks.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/delete.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/delete.svg new file mode 100644 index 0000000000..f2aea6e842 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/delete.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/discord.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/discord.svg new file mode 100644 index 0000000000..136239f7f2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/export.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/export.svg new file mode 100644 index 0000000000..d6314bd70a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/export.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/externaltools.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/externaltools.svg new file mode 100644 index 0000000000..c965b6c370 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/externaltools.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/help.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/help.svg new file mode 100644 index 0000000000..bcd14e0541 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/help.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/instance-settings.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/instance-settings.svg new file mode 100644 index 0000000000..69854d738a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/instance-settings.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/jarmods.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/jarmods.svg new file mode 100644 index 0000000000..72a8e504f3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/jarmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/java.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/java.svg new file mode 100644 index 0000000000..ff86c9ccc6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/java.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/language.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/language.svg new file mode 100644 index 0000000000..3d56d33e17 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/language.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/launch.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/launch.svg new file mode 100644 index 0000000000..678fd09882 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/launch.svg @@ -0,0 +1,8 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/loadermods.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/loadermods.svg new file mode 100644 index 0000000000..4fb0f96d9a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/loadermods.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/log.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/log.svg new file mode 100644 index 0000000000..cf9c9b2254 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/log.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/matrix.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/matrix.svg new file mode 100644 index 0000000000..4745efc111 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/matrix.svg @@ -0,0 +1,9 @@ + + + Matrix (protocol) logo + + + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/minecraft.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/minecraft.svg new file mode 100644 index 0000000000..1ffb4565f6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/minecraft.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/new.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/new.svg new file mode 100644 index 0000000000..6434a18e6e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/new.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/news.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/news.svg new file mode 100644 index 0000000000..3e3ebe9508 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/news.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/notes.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/notes.svg new file mode 100644 index 0000000000..a8eaf279bf --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/notes.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/patreon.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/patreon.svg new file mode 100644 index 0000000000..e12f1f8d9c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/patreon.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/proxy.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/proxy.svg new file mode 100644 index 0000000000..2e67ff6c92 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/proxy.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/reddit-alien.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/reddit-alien.svg new file mode 100644 index 0000000000..93b8eedc98 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/reddit-alien.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/refresh.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/refresh.svg new file mode 100644 index 0000000000..ecd2b394ef --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/refresh.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/rename.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/rename.svg new file mode 100644 index 0000000000..18ccc58a83 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/rename.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/resourcepacks.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/resourcepacks.svg new file mode 100644 index 0000000000..913d3c1fa0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/resourcepacks.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/screenshots.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/screenshots.svg new file mode 100644 index 0000000000..d984b33077 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/screenshots.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/server.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/server.svg new file mode 100644 index 0000000000..52d7dd7d2e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/server.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/settings.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/settings.svg new file mode 100644 index 0000000000..19e86e26e0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/settings.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/shaderpacks.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/shaderpacks.svg new file mode 100644 index 0000000000..591c6af5c7 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/shaderpacks.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/shortcut.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/shortcut.svg new file mode 100644 index 0000000000..426769d17b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/shortcut.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-bad.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-bad.svg new file mode 100644 index 0000000000..6fc3137e48 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-bad.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-good.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-good.svg new file mode 100644 index 0000000000..eb8bc03be4 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-good.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-yellow.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-yellow.svg new file mode 100644 index 0000000000..1dc4d0f51b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/status-yellow.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/tag.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/tag.svg new file mode 100644 index 0000000000..4887d126e8 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/tag.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/viewfolder.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/viewfolder.svg new file mode 100644 index 0000000000..4a8498ceb6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/viewfolder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/breeze_light/scalable/worlds.svg b/archived/projt-launcher/launcher/resources/breeze_light/scalable/worlds.svg new file mode 100644 index 0000000000..543cc55e4c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/breeze_light/scalable/worlds.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/documents/credits.html b/archived/projt-launcher/launcher/resources/documents/credits.html new file mode 100644 index 0000000000..ddf2547b02 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/documents/credits.html @@ -0,0 +1,45 @@ +
    +

    %1

    +

    YongDo-Hyun <GitHub>

    +

    grxtor <GitHub>

    +
    +

    %2

    +

    Sefa Eyeoglu (Scrumplex) <Website>

    +

    d-513 <GitHub>

    +

    txtsd <Website>

    +

    timoreo <GitHub>

    +

    ZekeZ <GitHub>

    +

    cozyGalvinism <GitHub>

    +

    DioEgizio <GitHub>

    +

    flowln <GitHub>

    +

    ViRb3 <GitHub>

    +

    Rachel Powers (Ryex) <GitHub>

    +

    TayouVR <GitHub>

    +

    TheKodeToad <GitHub>

    +

    getchoo <GitHub>

    +

    Alexandru Tripon (Trial97) <GitHub>

    +
    +

    %3

    +

    Andrew Okin <forkk@forkk.net>

    +

    Petr Mrázek <peterix@gmail.com>

    +

    Sky Welch <multimc@bunnies.io>

    +

    Jan (02JanDal) <02jandal@gmail.com>

    +

    RoboSky <@RoboSky_>

    +
    +

    %4

    +

    Boba <Website>

    +

    AutiOne <Website>

    +

    Fulmine <Website>

    +

    ely <GitHub>

    +

    gon sawa <GitHub>

    +

    Pankakes

    +

    tobimori <GitHub>

    +

    Orochimarufan <orochimarufan.x3@gmail.com>

    +

    TakSuyu <taksuyu@gmail.com>

    +

    Kilobyte <stiepen22@gmx.de>

    +

    Rootbear75 <@rootbear75>

    +

    Zeker Zhayard <@Zeker_Zhayard>

    +

    Everyone who helped establish our branding!

    +

    And everyone else who contributed!

    +
    +
    diff --git a/archived/projt-launcher/launcher/resources/documents/documents.qrc b/archived/projt-launcher/launcher/resources/documents/documents.qrc new file mode 100644 index 0000000000..62c82735e3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/documents/documents.qrc @@ -0,0 +1,8 @@ + + + + ../../../COPYING.md + credits.html + manifesto.md + + diff --git a/archived/projt-launcher/launcher/resources/documents/manifesto.md b/archived/projt-launcher/launcher/resources/documents/manifesto.md new file mode 100644 index 0000000000..a8945ceafb --- /dev/null +++ b/archived/projt-launcher/launcher/resources/documents/manifesto.md @@ -0,0 +1,108 @@ +## Overview: What Is Project Tick and Why It Exists? +Project Tick is a free software ecosystem designed for user freedom, legal clarity, and long term sustainability. It exists as a disciplined and pragmatic answer to the most common failure modes in open source. Open source projects often collapse due to unclear technical direction, rushed architectural decisions, and neglected CI/packaging pipelines. Project Tick was created to avoid those mistakes: decisions are documented, automation and reproducible builds are mandatory, and long term correctness wins over short term speed. + +This document summarizes Project Tick's purpose, core principles, products, infrastructure, and community model. It is both a description of how we work and a clear view of where we are headed. + +## Core Principles: Discipline and a Long Term Outlook +Project Tick rejects the "move fast, break fast" mindset in favor of durable and scalable solutions. Core principles: + +- Long Term Maintenance First: Code quality is never sacrificed for speed. Long term maintenance is more valuable than short term velocity. +- Enforced Architectural Boundaries: Architecture is explicitly defined and enforced. Disciplined modules beat fuzzy boundaries. +- CI/CD and Automation: Continuous integration and automation are first class requirements. Everything possible should be automated. +- Reproducible Builds: Build steps, inputs, and dependencies are deterministic so outputs match across environments. +- Respect for Licenses and Contributions: Upstream projects, licenses, and contributor intent are treated with care. Every change goes through technical and professional review; changes without clear rationale are rejected. + +These principles challenge the idea that "projects decay over time" and aim to keep the codebase resilient. + +## Why Project Tick? A Remedy to Open Source Pain Points +Common failures in open source include unclear direction, rushed architecture, neglected CI and packaging, undocumented changes, and hidden breakages. Project Tick stands against these with one rule: long term correctness is superior to short term comfort. Decisions are documented, automation is mandatory, and every change is questioned. This minimizes inconsistency and technical debt. + +## Product Ecosystem +Project Tick is not a single project; it is a disciplined software ecosystem. Major products and components include: + +### ProjT Launcher - Flagship +ProjT Launcher is the flagship product, a cross platform Minecraft launcher built for long term maintenance and architectural clarity. It is a fork of Prism Launcher focused on discipline and sustainability. + +- Long Term Maintenance: Architectural boundaries and review rules prevent uncontrolled technical debt. +- Controlled Third Party Integration: External dependencies, patch policies, and update procedures are maintained as documented, isolated forks. +- Deterministic CI and Builds: Dependency versions and build inputs are pinned for identical results across environments. +- Architectural Clarity: MVVM boundaries and a modular layout make review, refactoring, and long term contribution easier. + +ProjT Launcher is built with C++ and Qt, and provides reproducible builds via Nix and CMake. The repository is split into modules such as `launcher/` (app), `website/` (Eleventy site), `bot/` (automation), `meta/` (metadata generator), and `docs/` (documentation). The project vendors many forked libraries, including zlib, bzip2, quazip, cmark, tomlplusplus, and libqrencode. Other vendored components include GameMode integration, LocalPeer, murmur2 hashing, and terminal color utilities. + +### LauncherJava +LauncherJava is the Java component that actually starts Minecraft. It acts as the interface between the C++ launcher and the Minecraft JVM process. The launcher receives commands via a text based protocol, such as `mainClass`, `param`, `windowTitle`, and `sessionId`. There are two launcher types: standard (all modern versions) and legacy (pre 1.6). LauncherJava is built via CMake and can also be built with `./gradlew build`. + +### JavaCheck +JavaCheck is a minimal Java program that prints system properties. ProjT Launcher uses it to detect and validate Java installs, check runtime availability, version, architecture (32/64 bit), and auto detect installed JREs/JDKs. + +### bzip2 and zlib +ProjT Launcher maintains controlled forks of compression libraries. + +- bzip2/libbz2: Lossless block sorted compression. The fork stays compatible with upstream while moving forward in a controlled tree. Used for mod archive extraction, legacy Minecraft asset support, and large file download optimizations. +- zlib: Implements DEFLATE and provides ZIP/GZip functionality. Used for zip archive handling, PNG texture compression, network compression for mod downloads, and Minecraft world data (NBT). The fork is kept for long term maintenance and CI validation. + +These components are foundational for deterministic and high performance behavior. + +## Forge (Git) and Independent Infrastructure +Project Tick hosts its code on its own Forge platform. The "Sovereign Code" approach avoids dependence on third party platforms. Forge is optimized for high availability and speed, globally accessible, and community oriented. Contributors can join projects, fork, and collaborate in a transparent environment. + +## Mailing Lists and Community +Project Tick uses mailing lists for clarity and transparency. Each list has a clear purpose: + +- Announcements: Official announcements, release notices, security advisories, and major updates. Low traffic for readers, posting is restricted. +- Development (devel): Technical discussions, patch submissions, RFCs, architectural decisions, and infrastructure topics. +- Community: General discussion, questions, ideas, feedback, and coordination. The right starting point for newcomers. + +The community hub also contains categories for bug reports and PR review requests, but currently has no discussion threads. + +## Roadmap and Status +As of 2026, the roadmap is intentionally small and focused. Work is active on completing the GNU Make system. One completed item is a reported issue in the ProjT Launcher update system, announced to be fixed in version 0.0.4. No other items are planned, reflecting the "few but solid" approach. + +System status shows 100% uptime for global infrastructure; the mailing list system averages 1 ms latency with 100.0% uptime. No incidents were reported in the last 24 hours. This reflects a strong focus on stability. + +## Licensing and Legal Framework +Project Tick is ambitious not only in software, but in licensing as well. In addition to standard open source licenses, it offers specialized licenses for the AI and data era. + +### Project Tick General Public License (PT-GPL) +PT-GPL v1 is a copyleft license for software programs. It guarantees user freedoms to use, run, modify, and share the software, requires the license to accompany copies, and preserves copyright notices. It emphasizes access to reproducible forms and passing modification rights downstream. The preamble balances flexible use rights with the protection of authors and contributors. + +### Project Tick AI Use License (PT-AI) +PT-AI v2 defines how works may be used by AI systems. It prohibits "reconstruction" (reproducing a meaningful portion of a work). Allowed AI uses include indexing, classification, search, temporary analysis, and inference only. Training is permitted only for non commercial research, and deliberate storage in model parameters is forbidden. Commercial AI training is prohibited or requires a separate agreement. + +### Privacy Policy and Terms of Service +PT-PP (Privacy Policy) v2 guarantees minimal data collection and no sale of personal data. Collected data is limited to account information, form submissions, and technical data (IP, browser, OS). Data is used only for service operation, account management, security, and legal obligations. Cookies are used only for session management, authentication, and security; no ads or cross site tracking. + +PT-TOS (Terms of Service) v2 states that everyone accessing the website must follow these terms; they are separate from software licenses. Project Tick may restrict access for misuse. User submitted content may be used royalty free for operations and development; intellectual property rights belong to Project Tick, and no warranty or liability is provided. + +These documents define clear boundaries for both developers and users. + +## Design and Brand Identity +Project Tick visual identity is sharp, aggressive, and industrial. Color palette and typography: + +| Element | Description | Notes | +| --- | --- | --- | +| PT Red | #FF003C | Used for technical highlights and warnings | +| Deep Black | #050505 | Signature dark background for high contrast | +| Light Gray | #F0F0F0 | Light surfaces and reading areas | +| Slate Gray | #6A6E73 | Mid tone separation | +| Display Font | Space Grotesk | Geometric sans serif for titles and UI | +| Text Font | Red Hat Text | High readability for technical docs | + +Design principles include sharp, aggressive lines (0px border radius), high contrast, and a dark mode first approach. This aesthetic reflects a professional and serious stance. + +## New Website and Future Vision +On January 27, 2026, Project Tick launched a rebuilt website. This was not only a visual refresh but a dynamic foundation for a growing ecosystem. Highlights include: + +- Unified product structure: products, licenses, docs, and announcements are organized under one coherent model. +- Licensing and legal clarity: canonical license pages, version control, and dedicated areas for Terms and Privacy. +- Contributor management: a GitHub based Contributor License Agreement (CLA) system. +- Dynamic content system: products, handbooks, and news are managed dynamically for long term scalability and fast updates. +- Cleaner design: reduced noise, easier navigation, and clear separation between open source projects and platform services. + +The site provides a stable base for future products and features, with new additions planned incrementally. This reinforces the idea that Project Tick is not finished; it is a beginning. + +## Conclusion +Project Tick is an ecosystem built to address the lack of sustainability and discipline in open source. With a small but critical roadmap and a stable, transparent infrastructure, it offers a clear proposal: choose long term stability over short term speed, and disciplined evolution over random novelty. + +This overview outlines Project Tick's principles, products, and future. If you are considering joining, know that contributions are evaluated against high quality standards, clear justification, and long term maintenance requirements. Project Tick is not just a collection of software, but a clear vision of how open source should be. diff --git a/archived/projt-launcher/launcher/resources/flat/flat.qrc b/archived/projt-launcher/launcher/resources/flat/flat.qrc new file mode 100644 index 0000000000..2cc9f46f5a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/flat.qrc @@ -0,0 +1,54 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/cat.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/datapacks.svg + scalable/discord.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/packages.svg + scalable/proxy.svg + scalable/quickmods.svg + scalable/reddit-alien.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshot-placeholder.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/shortcut.svg + scalable/star.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-running.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/tag.svg + scalable/export.svg + scalable/rename.svg + scalable/server.svg + scalable/launch.svg + scalable/appearance.svg + + diff --git a/archived/projt-launcher/launcher/resources/flat/index.theme b/archived/projt-launcher/launcher/resources/flat/index.theme new file mode 100644 index 0000000000..34e27aa0f6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=Flat +Comment=Flat icons +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/about.svg b/archived/projt-launcher/launcher/resources/flat/scalable/about.svg new file mode 100644 index 0000000000..4f85045d0f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/about.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/accounts.svg b/archived/projt-launcher/launcher/resources/flat/scalable/accounts.svg new file mode 100644 index 0000000000..e6a1328dd3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/accounts.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/appearance.svg b/archived/projt-launcher/launcher/resources/flat/scalable/appearance.svg new file mode 100644 index 0000000000..11dcb3f330 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/appearance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/bug.svg b/archived/projt-launcher/launcher/resources/flat/scalable/bug.svg new file mode 100644 index 0000000000..ea370faab5 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/bug.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/cat.svg b/archived/projt-launcher/launcher/resources/flat/scalable/cat.svg new file mode 100644 index 0000000000..e90763b5c0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/cat.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/centralmods.svg b/archived/projt-launcher/launcher/resources/flat/scalable/centralmods.svg new file mode 100644 index 0000000000..c694662ad7 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/centralmods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/checkupdate.svg b/archived/projt-launcher/launcher/resources/flat/scalable/checkupdate.svg new file mode 100644 index 0000000000..e6525a08b6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/checkupdate.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/copy.svg b/archived/projt-launcher/launcher/resources/flat/scalable/copy.svg new file mode 100644 index 0000000000..36986e0d55 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/copy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/coremods.svg b/archived/projt-launcher/launcher/resources/flat/scalable/coremods.svg new file mode 100644 index 0000000000..21a3450e08 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/coremods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/custom-commands.svg b/archived/projt-launcher/launcher/resources/flat/scalable/custom-commands.svg new file mode 100644 index 0000000000..f2e587843a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/custom-commands.svg @@ -0,0 +1 @@ + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/datapacks.svg b/archived/projt-launcher/launcher/resources/flat/scalable/datapacks.svg new file mode 100644 index 0000000000..a35634b167 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/datapacks.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/delete.svg b/archived/projt-launcher/launcher/resources/flat/scalable/delete.svg new file mode 100644 index 0000000000..89a0948b1a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/delete.svg @@ -0,0 +1 @@ + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/discord.svg b/archived/projt-launcher/launcher/resources/flat/scalable/discord.svg new file mode 100644 index 0000000000..ad63180ff9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/discord.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/export.svg b/archived/projt-launcher/launcher/resources/flat/scalable/export.svg new file mode 100644 index 0000000000..a3b711a29c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/export.svg @@ -0,0 +1 @@ + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/externaltools.svg b/archived/projt-launcher/launcher/resources/flat/scalable/externaltools.svg new file mode 100644 index 0000000000..55820dfca5 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/externaltools.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/help.svg b/archived/projt-launcher/launcher/resources/flat/scalable/help.svg new file mode 100644 index 0000000000..26d5d7f466 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/help.svg @@ -0,0 +1,17 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/instance-settings.svg b/archived/projt-launcher/launcher/resources/flat/scalable/instance-settings.svg new file mode 100644 index 0000000000..dd9d86edd9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/instance-settings.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/jarmods.svg b/archived/projt-launcher/launcher/resources/flat/scalable/jarmods.svg new file mode 100644 index 0000000000..db90fa3455 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/jarmods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/java.svg b/archived/projt-launcher/launcher/resources/flat/scalable/java.svg new file mode 100644 index 0000000000..dc19ee2356 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/java.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/language.svg b/archived/projt-launcher/launcher/resources/flat/scalable/language.svg new file mode 100644 index 0000000000..f4d3f2f420 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/language.svg @@ -0,0 +1,103 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/launch.svg b/archived/projt-launcher/launcher/resources/flat/scalable/launch.svg new file mode 100644 index 0000000000..b462f2e450 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/launch.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/loadermods.svg b/archived/projt-launcher/launcher/resources/flat/scalable/loadermods.svg new file mode 100644 index 0000000000..8a2fd12cc5 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/loadermods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/log.svg b/archived/projt-launcher/launcher/resources/flat/scalable/log.svg new file mode 100644 index 0000000000..e8caa08ac2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/log.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/minecraft.svg b/archived/projt-launcher/launcher/resources/flat/scalable/minecraft.svg new file mode 100644 index 0000000000..c17c44cd99 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/minecraft.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/multimc.svg b/archived/projt-launcher/launcher/resources/flat/scalable/multimc.svg new file mode 100644 index 0000000000..1c1f2359d7 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/multimc.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/new.svg b/archived/projt-launcher/launcher/resources/flat/scalable/new.svg new file mode 100644 index 0000000000..01f19d7cc1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/new.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/news.svg b/archived/projt-launcher/launcher/resources/flat/scalable/news.svg new file mode 100644 index 0000000000..8868414e60 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/news.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/notes.svg b/archived/projt-launcher/launcher/resources/flat/scalable/notes.svg new file mode 100644 index 0000000000..ebe0cb5af4 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/notes.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/packages.svg b/archived/projt-launcher/launcher/resources/flat/scalable/packages.svg new file mode 100644 index 0000000000..fe576a4343 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/packages.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/patreon.svg b/archived/projt-launcher/launcher/resources/flat/scalable/patreon.svg new file mode 100644 index 0000000000..ad561f57b9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/patreon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/proxy.svg b/archived/projt-launcher/launcher/resources/flat/scalable/proxy.svg new file mode 100644 index 0000000000..4956fec8ff --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/proxy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/quickmods.svg b/archived/projt-launcher/launcher/resources/flat/scalable/quickmods.svg new file mode 100644 index 0000000000..952d1e0eca --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/quickmods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/reddit-alien.svg b/archived/projt-launcher/launcher/resources/flat/scalable/reddit-alien.svg new file mode 100644 index 0000000000..9bcfbedc8b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/reddit-alien.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/refresh.svg b/archived/projt-launcher/launcher/resources/flat/scalable/refresh.svg new file mode 100644 index 0000000000..94be1e2745 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/refresh.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/rename.svg b/archived/projt-launcher/launcher/resources/flat/scalable/rename.svg new file mode 100644 index 0000000000..d0b56723c9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/rename.svg @@ -0,0 +1 @@ + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/resourcepacks.svg b/archived/projt-launcher/launcher/resources/flat/scalable/resourcepacks.svg new file mode 100644 index 0000000000..b6054baf47 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/resourcepacks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/screenshot-placeholder.svg b/archived/projt-launcher/launcher/resources/flat/scalable/screenshot-placeholder.svg new file mode 100644 index 0000000000..99e0c17a6a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/screenshot-placeholder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/screenshots.svg b/archived/projt-launcher/launcher/resources/flat/scalable/screenshots.svg new file mode 100644 index 0000000000..208bb104fd --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/screenshots.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/server.svg b/archived/projt-launcher/launcher/resources/flat/scalable/server.svg new file mode 100644 index 0000000000..c1d09d29a2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/server.svg @@ -0,0 +1,44 @@ + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/settings.svg b/archived/projt-launcher/launcher/resources/flat/scalable/settings.svg new file mode 100644 index 0000000000..dd9d86edd9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/settings.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/shaderpacks.svg b/archived/projt-launcher/launcher/resources/flat/scalable/shaderpacks.svg new file mode 100644 index 0000000000..f1460bd96d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/shaderpacks.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/shortcut.svg b/archived/projt-launcher/launcher/resources/flat/scalable/shortcut.svg new file mode 100644 index 0000000000..83878d19f6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/shortcut.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/star.svg b/archived/projt-launcher/launcher/resources/flat/scalable/star.svg new file mode 100644 index 0000000000..878bdca8b8 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/star.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/status-bad.svg b/archived/projt-launcher/launcher/resources/flat/scalable/status-bad.svg new file mode 100644 index 0000000000..3f8e011611 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/status-bad.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/status-good.svg b/archived/projt-launcher/launcher/resources/flat/scalable/status-good.svg new file mode 100644 index 0000000000..3503d6bae8 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/status-good.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/status-running.svg b/archived/projt-launcher/launcher/resources/flat/scalable/status-running.svg new file mode 100644 index 0000000000..7c75031960 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/status-running.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/status-yellow.svg b/archived/projt-launcher/launcher/resources/flat/scalable/status-yellow.svg new file mode 100644 index 0000000000..ac2d234956 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/status-yellow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/tag.svg b/archived/projt-launcher/launcher/resources/flat/scalable/tag.svg new file mode 100644 index 0000000000..0629b185ff --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/tag.svg @@ -0,0 +1 @@ + diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/viewfolder.svg b/archived/projt-launcher/launcher/resources/flat/scalable/viewfolder.svg new file mode 100644 index 0000000000..2f5e29c95c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/viewfolder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat/scalable/worlds.svg b/archived/projt-launcher/launcher/resources/flat/scalable/worlds.svg new file mode 100644 index 0000000000..95a59bd4db --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat/scalable/worlds.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat_white/flat_white.qrc b/archived/projt-launcher/launcher/resources/flat_white/flat_white.qrc new file mode 100644 index 0000000000..d873e40d6e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/flat_white.qrc @@ -0,0 +1,54 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/cat.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/datapacks.svg + scalable/discord.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/packages.svg + scalable/proxy.svg + scalable/quickmods.svg + scalable/reddit-alien.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshot-placeholder.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/shortcut.svg + scalable/star.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-running.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/export.svg + scalable/rename.svg + scalable/tag.svg + scalable/launch.svg + scalable/server.svg + scalable/appearance.svg + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/index.theme b/archived/projt-launcher/launcher/resources/flat_white/index.theme new file mode 100644 index 0000000000..54dd0e102f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=Flat (White) +Comment=White version of the flat icons (dark mode) +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/about.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/about.svg new file mode 100644 index 0000000000..e2071c84f1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/about.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/accounts.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/accounts.svg new file mode 100644 index 0000000000..0b413e2aca --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/accounts.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/appearance.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/appearance.svg new file mode 100644 index 0000000000..b20d91f129 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/appearance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/bug.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/bug.svg new file mode 100644 index 0000000000..1e270acdb3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/bug.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/cat.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/cat.svg new file mode 100644 index 0000000000..93470c4ffc --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/cat.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/centralmods.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/centralmods.svg new file mode 100644 index 0000000000..277fe1115d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/centralmods.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/checkupdate.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/checkupdate.svg new file mode 100644 index 0000000000..78db2b0ccf --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/checkupdate.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/copy.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/copy.svg new file mode 100644 index 0000000000..abcb2b696b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/coremods.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/coremods.svg new file mode 100644 index 0000000000..f3132a5fd3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/coremods.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/custom-commands.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/custom-commands.svg new file mode 100644 index 0000000000..0ba459cff2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/custom-commands.svg @@ -0,0 +1 @@ + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/datapacks.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/datapacks.svg new file mode 100644 index 0000000000..fe1cf99874 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/datapacks.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/delete.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/delete.svg new file mode 100644 index 0000000000..653ecd3fe4 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/discord.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/discord.svg new file mode 100644 index 0000000000..6a07d2289f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/discord.svg @@ -0,0 +1,4 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/export.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/export.svg new file mode 100644 index 0000000000..0959521183 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/export.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/externaltools.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/externaltools.svg new file mode 100644 index 0000000000..d641f4f212 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/externaltools.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/help.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/help.svg new file mode 100644 index 0000000000..31e8c092b1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/help.svg @@ -0,0 +1,17 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/instance-settings.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/instance-settings.svg new file mode 100644 index 0000000000..95a0a80264 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/instance-settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/jarmods.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/jarmods.svg new file mode 100644 index 0000000000..603a8ae9ad --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/jarmods.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/java.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/java.svg new file mode 100644 index 0000000000..db81128e0d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/java.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/language.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/language.svg new file mode 100644 index 0000000000..4aef294685 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/language.svg @@ -0,0 +1,103 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/launch.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/launch.svg new file mode 100644 index 0000000000..ddd6d5f2ca --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/launch.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/loadermods.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/loadermods.svg new file mode 100644 index 0000000000..95c72084ae --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/loadermods.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/log.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/log.svg new file mode 100644 index 0000000000..a40139d36d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/log.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/minecraft.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/minecraft.svg new file mode 100644 index 0000000000..94aaebd13c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/minecraft.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/multimc.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/multimc.svg new file mode 100644 index 0000000000..9afe68d96f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/multimc.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/new.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/new.svg new file mode 100644 index 0000000000..22c6a6fe3a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/new.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/news.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/news.svg new file mode 100644 index 0000000000..76623f341f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/news.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/notes.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/notes.svg new file mode 100644 index 0000000000..18a1265de1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/notes.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/packages.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/packages.svg new file mode 100644 index 0000000000..d2c879557d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/packages.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/patreon.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/patreon.svg new file mode 100644 index 0000000000..d5385eac1f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/patreon.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/proxy.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/proxy.svg new file mode 100644 index 0000000000..30e27e8a45 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/proxy.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/quickmods.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/quickmods.svg new file mode 100644 index 0000000000..599bd2bf37 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/quickmods.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/reddit-alien.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/reddit-alien.svg new file mode 100644 index 0000000000..291b12e0ed --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/reddit-alien.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/refresh.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/refresh.svg new file mode 100644 index 0000000000..e8c6c44b38 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/rename.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/rename.svg new file mode 100644 index 0000000000..e7d6634a24 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/rename.svg @@ -0,0 +1,4 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/resourcepacks.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/resourcepacks.svg new file mode 100644 index 0000000000..272af76b7f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/resourcepacks.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/screenshot-placeholder.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/screenshot-placeholder.svg new file mode 100644 index 0000000000..162b78040c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/screenshot-placeholder.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/screenshots.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/screenshots.svg new file mode 100644 index 0000000000..ae1c876df0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/screenshots.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/server.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/server.svg new file mode 100644 index 0000000000..f41db1b22c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/settings.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/settings.svg new file mode 100644 index 0000000000..95a0a80264 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/shaderpacks.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/shaderpacks.svg new file mode 100644 index 0000000000..bfd8b83324 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/shaderpacks.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/shortcut.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/shortcut.svg new file mode 100644 index 0000000000..77ccbdd46f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/shortcut.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/star.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/star.svg new file mode 100644 index 0000000000..2a573ca30f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/status-bad.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-bad.svg new file mode 100644 index 0000000000..b6b42a9689 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-bad.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/status-good.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-good.svg new file mode 100644 index 0000000000..aee4c5234f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-good.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/status-running.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-running.svg new file mode 100644 index 0000000000..d4d5519440 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-running.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/status-yellow.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-yellow.svg new file mode 100644 index 0000000000..00737f515e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/status-yellow.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/tag.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/tag.svg new file mode 100644 index 0000000000..0d7661e0d9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/tag.svg @@ -0,0 +1,4 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/viewfolder.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/viewfolder.svg new file mode 100644 index 0000000000..b13c8eb360 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/viewfolder.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/flat_white/scalable/worlds.svg b/archived/projt-launcher/launcher/resources/flat_white/scalable/worlds.svg new file mode 100644 index 0000000000..d7aaef1d56 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/flat_white/scalable/worlds.svg @@ -0,0 +1,3 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/iOS.qrc b/archived/projt-launcher/launcher/resources/iOS/iOS.qrc new file mode 100644 index 0000000000..9b8d84f503 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/iOS.qrc @@ -0,0 +1,43 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/tag.svg + scalable/export.svg + scalable/rename.svg + scalable/launch.svg + scalable/shortcut.svg + + diff --git a/archived/projt-launcher/launcher/resources/iOS/index.theme b/archived/projt-launcher/launcher/resources/iOS/index.theme new file mode 100644 index 0000000000..b0f2f6bac2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=iOS +Comment=iOS theme by pexner +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/about.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/about.svg new file mode 100644 index 0000000000..c4d354710a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/about.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/accounts.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/accounts.svg new file mode 100644 index 0000000000..65f76c3f41 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/accounts.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/bug.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/bug.svg new file mode 100644 index 0000000000..fc4a3d6904 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/bug.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/centralmods.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/centralmods.svg new file mode 100644 index 0000000000..1b4c474194 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/centralmods.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/checkupdate.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/checkupdate.svg new file mode 100644 index 0000000000..9fc983d14b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/checkupdate.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/copy.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/copy.svg new file mode 100644 index 0000000000..3ccc2f0659 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/copy.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/coremods.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/coremods.svg new file mode 100644 index 0000000000..ea47872c3a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/coremods.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/custom-commands.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/custom-commands.svg new file mode 100644 index 0000000000..f44e2bfe28 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/custom-commands.svg @@ -0,0 +1,63 @@ + +image/svg+xml + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/delete.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/delete.svg new file mode 100644 index 0000000000..a542fa4ff5 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/delete.svg @@ -0,0 +1,31 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/export.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/export.svg new file mode 100644 index 0000000000..db2f4c3cb2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/export.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/externaltools.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/externaltools.svg new file mode 100644 index 0000000000..16e9fa4880 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/externaltools.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/help.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/help.svg new file mode 100644 index 0000000000..9c2d2e9323 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/help.svg @@ -0,0 +1,38 @@ + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/instance-settings.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/instance-settings.svg new file mode 100644 index 0000000000..95b8a50815 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/instance-settings.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/jarmods.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/jarmods.svg new file mode 100644 index 0000000000..c4c5ca8c9c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/jarmods.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/java.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/java.svg new file mode 100644 index 0000000000..8d7c279876 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/java.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/language.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/language.svg new file mode 100644 index 0000000000..fcc3436e97 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/language.svg @@ -0,0 +1,32 @@ + +image/svg+xml + + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/launch.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/launch.svg new file mode 100644 index 0000000000..c16d5c37ce --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/launch.svg @@ -0,0 +1,17 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/loadermods.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/loadermods.svg new file mode 100644 index 0000000000..010efa11fc --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/loadermods.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/log.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/log.svg new file mode 100644 index 0000000000..5d1c7f0615 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/log.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/minecraft.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/minecraft.svg new file mode 100644 index 0000000000..069b4e710b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/minecraft.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/multimc.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/multimc.svg new file mode 100644 index 0000000000..bc81943352 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/multimc.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/new.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/new.svg new file mode 100644 index 0000000000..9f221580f6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/new.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/news.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/news.svg new file mode 100644 index 0000000000..d3c010bb2a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/news.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/notes.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/notes.svg new file mode 100644 index 0000000000..b42ebeef5f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/notes.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/patreon.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/patreon.svg new file mode 100644 index 0000000000..1bd06f4aee --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/patreon.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/proxy.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/proxy.svg new file mode 100644 index 0000000000..f655228170 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/proxy.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/refresh.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/refresh.svg new file mode 100644 index 0000000000..297b79c9b6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/refresh.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/rename.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/rename.svg new file mode 100644 index 0000000000..064e84b744 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/rename.svg @@ -0,0 +1,16 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/resourcepacks.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/resourcepacks.svg new file mode 100644 index 0000000000..5b359d63bf --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/resourcepacks.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/screenshots.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/screenshots.svg new file mode 100644 index 0000000000..39ce7b8274 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/screenshots.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/settings.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/settings.svg new file mode 100644 index 0000000000..95b8a50815 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/settings.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/shaderpacks.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/shaderpacks.svg new file mode 100644 index 0000000000..a2aa1b21de --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/shaderpacks.svg @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/shortcut.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/shortcut.svg new file mode 100644 index 0000000000..16e9fa4880 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/shortcut.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/status-bad.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/status-bad.svg new file mode 100644 index 0000000000..4019c8dabf --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/status-bad.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/status-good.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/status-good.svg new file mode 100644 index 0000000000..e185911329 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/status-good.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/status-yellow.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/status-yellow.svg new file mode 100644 index 0000000000..d8a28e239e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/status-yellow.svg @@ -0,0 +1,56 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/tag.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/tag.svg new file mode 100644 index 0000000000..23b549e530 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/tag.svg @@ -0,0 +1,20 @@ + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/viewfolder.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/viewfolder.svg new file mode 100644 index 0000000000..0ae0c0b52d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/viewfolder.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/iOS/scalable/worlds.svg b/archived/projt-launcher/launcher/resources/iOS/scalable/worlds.svg new file mode 100644 index 0000000000..1596fd760d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/iOS/scalable/worlds.svg @@ -0,0 +1,44 @@ + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/chicken_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/chicken_legacy.png new file mode 100644 index 0000000000..b4945d75a7 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/chicken_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/creeper_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/creeper_legacy.png new file mode 100644 index 0000000000..92d9231326 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/creeper_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png new file mode 100644 index 0000000000..fd910da47c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/flame_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/flame_legacy.png new file mode 100644 index 0000000000..3dd8500c6b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/flame_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/forge.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/forge.png new file mode 100644 index 0000000000..10c5f8d6bc Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/forge.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_glow.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_glow.png new file mode 100644 index 0000000000..a8bfbbb962 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_glow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png new file mode 100644 index 0000000000..01aa4d5172 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/gear_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/gear_legacy.png new file mode 100644 index 0000000000..bb46fe0262 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/gear_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/herobrine_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/herobrine_legacy.png new file mode 100644 index 0000000000..d25d1b1b18 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/herobrine_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/infinity_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/infinity_legacy.png new file mode 100644 index 0000000000..322ab43611 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/infinity_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/liteloader.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/liteloader.png new file mode 100644 index 0000000000..acd977d7ef Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/liteloader.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/magitech_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/magitech_legacy.png new file mode 100644 index 0000000000..c83d0c948a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/magitech_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/meat_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/meat_legacy.png new file mode 100644 index 0000000000..14a50bec0f Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/meat_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/netherstar_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/netherstar_legacy.png new file mode 100644 index 0000000000..86cc87b4ae Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/netherstar_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/skeleton_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/skeleton_legacy.png new file mode 100644 index 0000000000..416ca66e0e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/skeleton_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png new file mode 100644 index 0000000000..b7e2bdc135 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/instances/steve_legacy.png b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/steve_legacy.png new file mode 100644 index 0000000000..afe8aaf462 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/instances/steve_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/shaderpacks.png b/archived/projt-launcher/launcher/resources/multimc/128x128/shaderpacks.png new file mode 100644 index 0000000000..d2f1c03283 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/shaderpacks.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/128x128/unknown_server.png b/archived/projt-launcher/launcher/resources/multimc/128x128/unknown_server.png new file mode 100644 index 0000000000..b9761e08f1 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/128x128/unknown_server.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/bug.png b/archived/projt-launcher/launcher/resources/multimc/16x16/bug.png new file mode 100644 index 0000000000..57e7d82036 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/bug.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/cat.png b/archived/projt-launcher/launcher/resources/multimc/16x16/cat.png new file mode 100644 index 0000000000..73d5fa856e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/cat.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/centralmods.png b/archived/projt-launcher/launcher/resources/multimc/16x16/centralmods.png new file mode 100644 index 0000000000..0a573fb4ec Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/centralmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/copy.png b/archived/projt-launcher/launcher/resources/multimc/16x16/copy.png new file mode 100644 index 0000000000..24251adcfa Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/copy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/coremods.png b/archived/projt-launcher/launcher/resources/multimc/16x16/coremods.png new file mode 100644 index 0000000000..3d3932dbeb Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/coremods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/help.png b/archived/projt-launcher/launcher/resources/multimc/16x16/help.png new file mode 100644 index 0000000000..3dee5a3f91 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/help.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/instance-settings.png b/archived/projt-launcher/launcher/resources/multimc/16x16/instance-settings.png new file mode 100644 index 0000000000..6c9073b967 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/instance-settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/jarmods.png b/archived/projt-launcher/launcher/resources/multimc/16x16/jarmods.png new file mode 100644 index 0000000000..cdcbe788b9 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/jarmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/loadermods.png b/archived/projt-launcher/launcher/resources/multimc/16x16/loadermods.png new file mode 100644 index 0000000000..ad0e6237df Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/loadermods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/log.png b/archived/projt-launcher/launcher/resources/multimc/16x16/log.png new file mode 100644 index 0000000000..74324047e5 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/log.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/minecraft.png b/archived/projt-launcher/launcher/resources/multimc/16x16/minecraft.png new file mode 100644 index 0000000000..3de54f74a7 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/minecraft.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/new.png b/archived/projt-launcher/launcher/resources/multimc/16x16/new.png new file mode 100644 index 0000000000..dfde06f61a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/new.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/news.png b/archived/projt-launcher/launcher/resources/multimc/16x16/news.png new file mode 100644 index 0000000000..04e016da7a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/news.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/noaccount.png b/archived/projt-launcher/launcher/resources/multimc/16x16/noaccount.png new file mode 100644 index 0000000000..544d68207d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/noaccount.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/patreon.png b/archived/projt-launcher/launcher/resources/multimc/16x16/patreon.png new file mode 100644 index 0000000000..0c306e7ccb Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/patreon.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/refresh.png b/archived/projt-launcher/launcher/resources/multimc/16x16/refresh.png new file mode 100644 index 0000000000..2e81c92467 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/refresh.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/resourcepacks.png b/archived/projt-launcher/launcher/resources/multimc/16x16/resourcepacks.png new file mode 100644 index 0000000000..ac4c5dc437 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/resourcepacks.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/screenshots.png b/archived/projt-launcher/launcher/resources/multimc/16x16/screenshots.png new file mode 100644 index 0000000000..f0e5e439ea Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/screenshots.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/settings.png b/archived/projt-launcher/launcher/resources/multimc/16x16/settings.png new file mode 100644 index 0000000000..6c9073b967 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/star.png b/archived/projt-launcher/launcher/resources/multimc/16x16/star.png new file mode 100644 index 0000000000..20278be0cb Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/star.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/status-bad.png b/archived/projt-launcher/launcher/resources/multimc/16x16/status-bad.png new file mode 100644 index 0000000000..c71142b8ab Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/status-bad.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/status-good.png b/archived/projt-launcher/launcher/resources/multimc/16x16/status-good.png new file mode 100644 index 0000000000..456a67c5b7 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/status-good.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/status-running.png b/archived/projt-launcher/launcher/resources/multimc/16x16/status-running.png new file mode 100644 index 0000000000..7b7bfec91e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/status-running.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/status-yellow.png b/archived/projt-launcher/launcher/resources/multimc/16x16/status-yellow.png new file mode 100644 index 0000000000..f652ddae22 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/status-yellow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/viewfolder.png b/archived/projt-launcher/launcher/resources/multimc/16x16/viewfolder.png new file mode 100644 index 0000000000..f5f4014272 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/viewfolder.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/16x16/worlds.png b/archived/projt-launcher/launcher/resources/multimc/16x16/worlds.png new file mode 100644 index 0000000000..ed4249ec37 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/16x16/worlds.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/bug.png b/archived/projt-launcher/launcher/resources/multimc/22x22/bug.png new file mode 100644 index 0000000000..8aeb25d66b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/bug.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/cat.png b/archived/projt-launcher/launcher/resources/multimc/22x22/cat.png new file mode 100644 index 0000000000..a5795b9b89 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/cat.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/centralmods.png b/archived/projt-launcher/launcher/resources/multimc/22x22/centralmods.png new file mode 100644 index 0000000000..a54fdb0b04 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/centralmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/copy.png b/archived/projt-launcher/launcher/resources/multimc/22x22/copy.png new file mode 100644 index 0000000000..5cdc69dbeb Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/copy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/help.png b/archived/projt-launcher/launcher/resources/multimc/22x22/help.png new file mode 100644 index 0000000000..db49f9e31d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/help.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/instance-settings.png b/archived/projt-launcher/launcher/resources/multimc/22x22/instance-settings.png new file mode 100644 index 0000000000..8eb9ee49de Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/instance-settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/news.png b/archived/projt-launcher/launcher/resources/multimc/22x22/news.png new file mode 100644 index 0000000000..46eaab8698 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/news.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/patreon.png b/archived/projt-launcher/launcher/resources/multimc/22x22/patreon.png new file mode 100644 index 0000000000..8da8780ec2 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/patreon.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/refresh.png b/archived/projt-launcher/launcher/resources/multimc/22x22/refresh.png new file mode 100644 index 0000000000..f517f7acef Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/refresh.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/screenshots.png b/archived/projt-launcher/launcher/resources/multimc/22x22/screenshots.png new file mode 100644 index 0000000000..780eb43515 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/screenshots.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/settings.png b/archived/projt-launcher/launcher/resources/multimc/22x22/settings.png new file mode 100644 index 0000000000..8eb9ee49de Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/status-bad.png b/archived/projt-launcher/launcher/resources/multimc/22x22/status-bad.png new file mode 100644 index 0000000000..9d001ccd2d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/status-bad.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/status-good.png b/archived/projt-launcher/launcher/resources/multimc/22x22/status-good.png new file mode 100644 index 0000000000..9ac765abe1 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/status-good.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/status-running.png b/archived/projt-launcher/launcher/resources/multimc/22x22/status-running.png new file mode 100644 index 0000000000..21caa06b89 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/status-running.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/status-yellow.png b/archived/projt-launcher/launcher/resources/multimc/22x22/status-yellow.png new file mode 100644 index 0000000000..e125cf098a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/status-yellow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/viewfolder.png b/archived/projt-launcher/launcher/resources/multimc/22x22/viewfolder.png new file mode 100644 index 0000000000..7065e9ac93 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/viewfolder.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/22x22/worlds.png b/archived/projt-launcher/launcher/resources/multimc/22x22/worlds.png new file mode 100644 index 0000000000..ebb32f10cd Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/22x22/worlds.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/cat.png b/archived/projt-launcher/launcher/resources/multimc/24x24/cat.png new file mode 100644 index 0000000000..08b0ab1b06 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/cat.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/coremods.png b/archived/projt-launcher/launcher/resources/multimc/24x24/coremods.png new file mode 100644 index 0000000000..0cbd3f173a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/coremods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/jarmods.png b/archived/projt-launcher/launcher/resources/multimc/24x24/jarmods.png new file mode 100644 index 0000000000..a4824c2764 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/jarmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/loadermods.png b/archived/projt-launcher/launcher/resources/multimc/24x24/loadermods.png new file mode 100644 index 0000000000..cd4954d5a7 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/loadermods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/log.png b/archived/projt-launcher/launcher/resources/multimc/24x24/log.png new file mode 100644 index 0000000000..7978968c1e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/log.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/minecraft.png b/archived/projt-launcher/launcher/resources/multimc/24x24/minecraft.png new file mode 100644 index 0000000000..8869844cb4 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/minecraft.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/noaccount.png b/archived/projt-launcher/launcher/resources/multimc/24x24/noaccount.png new file mode 100644 index 0000000000..05d5cc584b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/noaccount.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/patreon.png b/archived/projt-launcher/launcher/resources/multimc/24x24/patreon.png new file mode 100644 index 0000000000..2e1cc05480 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/patreon.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/resourcepacks.png b/archived/projt-launcher/launcher/resources/multimc/24x24/resourcepacks.png new file mode 100644 index 0000000000..b434fb124b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/resourcepacks.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/star.png b/archived/projt-launcher/launcher/resources/multimc/24x24/star.png new file mode 100644 index 0000000000..8527f5092e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/star.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/status-bad.png b/archived/projt-launcher/launcher/resources/multimc/24x24/status-bad.png new file mode 100644 index 0000000000..eae695286a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/status-bad.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/status-good.png b/archived/projt-launcher/launcher/resources/multimc/24x24/status-good.png new file mode 100644 index 0000000000..e315beaf3c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/status-good.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/status-running.png b/archived/projt-launcher/launcher/resources/multimc/24x24/status-running.png new file mode 100644 index 0000000000..9c60594623 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/status-running.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/24x24/status-yellow.png b/archived/projt-launcher/launcher/resources/multimc/24x24/status-yellow.png new file mode 100644 index 0000000000..118efd8903 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/24x24/status-yellow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/256x256/minecraft.png b/archived/projt-launcher/launcher/resources/multimc/256x256/minecraft.png new file mode 100644 index 0000000000..0b24f5501a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/256x256/minecraft.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/bug.png b/archived/projt-launcher/launcher/resources/multimc/32x32/bug.png new file mode 100644 index 0000000000..3d97be84a1 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/bug.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/cat.png b/archived/projt-launcher/launcher/resources/multimc/32x32/cat.png new file mode 100644 index 0000000000..b9b21e6634 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/cat.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/centralmods.png b/archived/projt-launcher/launcher/resources/multimc/32x32/centralmods.png new file mode 100644 index 0000000000..7225ba08c5 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/centralmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/copy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/copy.png new file mode 100644 index 0000000000..ce662604ea Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/copy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/coremods.png b/archived/projt-launcher/launcher/resources/multimc/32x32/coremods.png new file mode 100644 index 0000000000..9718ec6714 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/coremods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/help.png b/archived/projt-launcher/launcher/resources/multimc/32x32/help.png new file mode 100644 index 0000000000..6e4cdbff61 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/help.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instance-settings.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instance-settings.png new file mode 100644 index 0000000000..4be48c1d5d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instance-settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/brick_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/brick_legacy.png new file mode 100644 index 0000000000..7d35f4da58 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/brick_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/chicken_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/chicken_legacy.png new file mode 100644 index 0000000000..7991410e18 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/chicken_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/creeper_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/creeper_legacy.png new file mode 100644 index 0000000000..571d2de197 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/creeper_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/diamond_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/diamond_legacy.png new file mode 100644 index 0000000000..3ad9c002f3 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/diamond_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/dirt_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/dirt_legacy.png new file mode 100644 index 0000000000..719a45ed59 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/dirt_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png new file mode 100644 index 0000000000..e0262f659f Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_glow.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_glow.png new file mode 100644 index 0000000000..7437b27cc1 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_glow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png new file mode 100644 index 0000000000..a70109bbb9 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/gear_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/gear_legacy.png new file mode 100644 index 0000000000..61dc9f500d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/gear_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/gold_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/gold_legacy.png new file mode 100644 index 0000000000..99d91795cd Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/gold_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/grass_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/grass_legacy.png new file mode 100644 index 0000000000..400f210675 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/grass_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/herobrine_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/herobrine_legacy.png new file mode 100644 index 0000000000..8ed872a6f4 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/herobrine_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/infinity_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/infinity_legacy.png new file mode 100644 index 0000000000..62291c782e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/infinity_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/iron_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/iron_legacy.png new file mode 100644 index 0000000000..d05d7c01e8 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/iron_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/magitech_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/magitech_legacy.png new file mode 100644 index 0000000000..bd630da8f6 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/magitech_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/meat_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/meat_legacy.png new file mode 100644 index 0000000000..422c88eebf Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/meat_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/netherstar_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/netherstar_legacy.png new file mode 100644 index 0000000000..6f5c6f22b4 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/netherstar_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/planks_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/planks_legacy.png new file mode 100644 index 0000000000..0ff6d19b0c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/planks_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/skeleton_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/skeleton_legacy.png new file mode 100644 index 0000000000..2327a036a7 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/skeleton_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png new file mode 100644 index 0000000000..258c9b34d6 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/steve_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/steve_legacy.png new file mode 100644 index 0000000000..3467335f0a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/steve_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/stone_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/stone_legacy.png new file mode 100644 index 0000000000..7a4d88cf04 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/stone_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/instances/tnt_legacy.png b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/tnt_legacy.png new file mode 100644 index 0000000000..7ab83644fb Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/instances/tnt_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/jarmods.png b/archived/projt-launcher/launcher/resources/multimc/32x32/jarmods.png new file mode 100644 index 0000000000..848be629fa Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/jarmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/loadermods.png b/archived/projt-launcher/launcher/resources/multimc/32x32/loadermods.png new file mode 100644 index 0000000000..73d70a30a6 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/loadermods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/log.png b/archived/projt-launcher/launcher/resources/multimc/32x32/log.png new file mode 100644 index 0000000000..6c2290f770 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/log.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/minecraft.png b/archived/projt-launcher/launcher/resources/multimc/32x32/minecraft.png new file mode 100644 index 0000000000..6b36426922 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/minecraft.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/news.png b/archived/projt-launcher/launcher/resources/multimc/32x32/news.png new file mode 100644 index 0000000000..2408031240 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/news.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/noaccount.png b/archived/projt-launcher/launcher/resources/multimc/32x32/noaccount.png new file mode 100644 index 0000000000..98ca7130e0 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/noaccount.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/patreon.png b/archived/projt-launcher/launcher/resources/multimc/32x32/patreon.png new file mode 100644 index 0000000000..440195d2e8 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/patreon.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/refresh.png b/archived/projt-launcher/launcher/resources/multimc/32x32/refresh.png new file mode 100644 index 0000000000..e67c5fe51b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/refresh.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/resourcepacks.png b/archived/projt-launcher/launcher/resources/multimc/32x32/resourcepacks.png new file mode 100644 index 0000000000..8af7fe31f0 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/resourcepacks.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/screenshots.png b/archived/projt-launcher/launcher/resources/multimc/32x32/screenshots.png new file mode 100644 index 0000000000..95c8c7e935 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/screenshots.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/settings.png b/archived/projt-launcher/launcher/resources/multimc/32x32/settings.png new file mode 100644 index 0000000000..4be48c1d5d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/star.png b/archived/projt-launcher/launcher/resources/multimc/32x32/star.png new file mode 100644 index 0000000000..c797ab34c8 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/star.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/status-bad.png b/archived/projt-launcher/launcher/resources/multimc/32x32/status-bad.png new file mode 100644 index 0000000000..77ac8fe01e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/status-bad.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/status-good.png b/archived/projt-launcher/launcher/resources/multimc/32x32/status-good.png new file mode 100644 index 0000000000..b8f7095ade Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/status-good.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/status-running.png b/archived/projt-launcher/launcher/resources/multimc/32x32/status-running.png new file mode 100644 index 0000000000..8ff17a046f Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/status-running.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/status-yellow.png b/archived/projt-launcher/launcher/resources/multimc/32x32/status-yellow.png new file mode 100644 index 0000000000..36270afb9a Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/status-yellow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/viewfolder.png b/archived/projt-launcher/launcher/resources/multimc/32x32/viewfolder.png new file mode 100644 index 0000000000..32d7b4bae2 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/viewfolder.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/32x32/worlds.png b/archived/projt-launcher/launcher/resources/multimc/32x32/worlds.png new file mode 100644 index 0000000000..dce4d96b52 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/32x32/worlds.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/bug.png b/archived/projt-launcher/launcher/resources/multimc/48x48/bug.png new file mode 100644 index 0000000000..8de0b0755f Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/bug.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/cat.png b/archived/projt-launcher/launcher/resources/multimc/48x48/cat.png new file mode 100644 index 0000000000..f84221d7a2 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/cat.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/centralmods.png b/archived/projt-launcher/launcher/resources/multimc/48x48/centralmods.png new file mode 100644 index 0000000000..2425a7c747 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/centralmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/copy.png b/archived/projt-launcher/launcher/resources/multimc/48x48/copy.png new file mode 100644 index 0000000000..4dc04b0805 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/copy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/help.png b/archived/projt-launcher/launcher/resources/multimc/48x48/help.png new file mode 100644 index 0000000000..f57c6c8968 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/help.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/instance-settings.png b/archived/projt-launcher/launcher/resources/multimc/48x48/instance-settings.png new file mode 100644 index 0000000000..ec298cd62d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/instance-settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/log.png b/archived/projt-launcher/launcher/resources/multimc/48x48/log.png new file mode 100644 index 0000000000..dc3eb4e275 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/log.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/minecraft.png b/archived/projt-launcher/launcher/resources/multimc/48x48/minecraft.png new file mode 100644 index 0000000000..4fe522ffb1 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/minecraft.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/news.png b/archived/projt-launcher/launcher/resources/multimc/48x48/news.png new file mode 100644 index 0000000000..d2f5d178a5 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/news.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/noaccount.png b/archived/projt-launcher/launcher/resources/multimc/48x48/noaccount.png new file mode 100644 index 0000000000..c13e4d6d5b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/noaccount.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/patreon.png b/archived/projt-launcher/launcher/resources/multimc/48x48/patreon.png new file mode 100644 index 0000000000..7e8f253670 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/patreon.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/refresh.png b/archived/projt-launcher/launcher/resources/multimc/48x48/refresh.png new file mode 100644 index 0000000000..87e113583f Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/refresh.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/screenshots.png b/archived/projt-launcher/launcher/resources/multimc/48x48/screenshots.png new file mode 100644 index 0000000000..694b96cd95 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/screenshots.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/settings.png b/archived/projt-launcher/launcher/resources/multimc/48x48/settings.png new file mode 100644 index 0000000000..ec298cd62d Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/star.png b/archived/projt-launcher/launcher/resources/multimc/48x48/star.png new file mode 100644 index 0000000000..c5253c334b Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/star.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/status-bad.png b/archived/projt-launcher/launcher/resources/multimc/48x48/status-bad.png new file mode 100644 index 0000000000..083506d289 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/status-bad.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/status-good.png b/archived/projt-launcher/launcher/resources/multimc/48x48/status-good.png new file mode 100644 index 0000000000..0c3377ad72 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/status-good.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/status-running.png b/archived/projt-launcher/launcher/resources/multimc/48x48/status-running.png new file mode 100644 index 0000000000..94598c9277 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/status-running.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/status-yellow.png b/archived/projt-launcher/launcher/resources/multimc/48x48/status-yellow.png new file mode 100644 index 0000000000..bb76fcd692 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/status-yellow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/viewfolder.png b/archived/projt-launcher/launcher/resources/multimc/48x48/viewfolder.png new file mode 100644 index 0000000000..2245ba30a0 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/viewfolder.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/48x48/worlds.png b/archived/projt-launcher/launcher/resources/multimc/48x48/worlds.png new file mode 100644 index 0000000000..eb44150a31 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/48x48/worlds.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/50x50/instances/enderman_legacy.png b/archived/projt-launcher/launcher/resources/multimc/50x50/instances/enderman_legacy.png new file mode 100644 index 0000000000..36c791eb0c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/50x50/instances/enderman_legacy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/bug.png b/archived/projt-launcher/launcher/resources/multimc/64x64/bug.png new file mode 100644 index 0000000000..6c9ac6af20 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/bug.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/cat.png b/archived/projt-launcher/launcher/resources/multimc/64x64/cat.png new file mode 100644 index 0000000000..65681e6b86 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/cat.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/centralmods.png b/archived/projt-launcher/launcher/resources/multimc/64x64/centralmods.png new file mode 100644 index 0000000000..d307356017 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/centralmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/copy.png b/archived/projt-launcher/launcher/resources/multimc/64x64/copy.png new file mode 100644 index 0000000000..69fa1c3fbd Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/copy.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/coremods.png b/archived/projt-launcher/launcher/resources/multimc/64x64/coremods.png new file mode 100644 index 0000000000..b1b1f82373 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/coremods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/help.png b/archived/projt-launcher/launcher/resources/multimc/64x64/help.png new file mode 100644 index 0000000000..e419f86008 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/help.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/instance-settings.png b/archived/projt-launcher/launcher/resources/multimc/64x64/instance-settings.png new file mode 100644 index 0000000000..9df7fe9bcd Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/instance-settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/jarmods.png b/archived/projt-launcher/launcher/resources/multimc/64x64/jarmods.png new file mode 100644 index 0000000000..5abd5ecc55 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/jarmods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/loadermods.png b/archived/projt-launcher/launcher/resources/multimc/64x64/loadermods.png new file mode 100644 index 0000000000..485aa843ab Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/loadermods.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/log.png b/archived/projt-launcher/launcher/resources/multimc/64x64/log.png new file mode 100644 index 0000000000..decee34bd1 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/log.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/news.png b/archived/projt-launcher/launcher/resources/multimc/64x64/news.png new file mode 100644 index 0000000000..a1c28fdd63 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/news.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/patreon.png b/archived/projt-launcher/launcher/resources/multimc/64x64/patreon.png new file mode 100644 index 0000000000..5c2d88814c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/patreon.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/refresh.png b/archived/projt-launcher/launcher/resources/multimc/64x64/refresh.png new file mode 100644 index 0000000000..737bd05811 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/refresh.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/resourcepacks.png b/archived/projt-launcher/launcher/resources/multimc/64x64/resourcepacks.png new file mode 100644 index 0000000000..703fde6b5c Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/resourcepacks.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/screenshots.png b/archived/projt-launcher/launcher/resources/multimc/64x64/screenshots.png new file mode 100644 index 0000000000..a57bf27728 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/screenshots.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/settings.png b/archived/projt-launcher/launcher/resources/multimc/64x64/settings.png new file mode 100644 index 0000000000..9df7fe9bcd Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/settings.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/star.png b/archived/projt-launcher/launcher/resources/multimc/64x64/star.png new file mode 100644 index 0000000000..24e9d75c7e Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/star.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/status-bad.png b/archived/projt-launcher/launcher/resources/multimc/64x64/status-bad.png new file mode 100644 index 0000000000..669d3159db Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/status-bad.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/status-good.png b/archived/projt-launcher/launcher/resources/multimc/64x64/status-good.png new file mode 100644 index 0000000000..4d256cc048 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/status-good.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/status-running.png b/archived/projt-launcher/launcher/resources/multimc/64x64/status-running.png new file mode 100644 index 0000000000..64d6d0a8db Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/status-running.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/status-yellow.png b/archived/projt-launcher/launcher/resources/multimc/64x64/status-yellow.png new file mode 100644 index 0000000000..98013151b8 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/status-yellow.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/viewfolder.png b/archived/projt-launcher/launcher/resources/multimc/64x64/viewfolder.png new file mode 100644 index 0000000000..d16cacc4da Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/viewfolder.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/64x64/worlds.png b/archived/projt-launcher/launcher/resources/multimc/64x64/worlds.png new file mode 100644 index 0000000000..25aa1d6855 Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/64x64/worlds.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/8x8/noaccount.png b/archived/projt-launcher/launcher/resources/multimc/8x8/noaccount.png new file mode 100644 index 0000000000..645ea1bede Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/8x8/noaccount.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/index.theme b/archived/projt-launcher/launcher/resources/multimc/index.theme new file mode 100644 index 0000000000..497106d6f1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/index.theme @@ -0,0 +1,57 @@ +[Icon Theme] +Name=Legacy +Comment=Default Icons +Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances + +[8x8] +Size=8 + +[16x16] +Size=16 + +[22x22] +Size=22 + +[24x24] +Size=24 + +[32x32] +Size=32 + +[32x32/instances] +Size=32 +MinSize=1 +MaxSize=32 + +[48x48] +Size=48 + +[50x50/instances] +Size=50 + +[64x64] +Size=64 + +[128x128] +Size=128 +MinSize=33 +MaxSize=128 + +[128x128/instances] +Size=128 +MinSize=33 +MaxSize=128 + +[256x256] +Size=256 + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 + +[scalable/instances] +Size=128 +MinSize=16 +MaxSize=256 diff --git a/archived/projt-launcher/launcher/resources/multimc/multimc.qrc b/archived/projt-launcher/launcher/resources/multimc/multimc.qrc new file mode 100644 index 0000000000..3b2c59ab1d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/multimc.qrc @@ -0,0 +1,340 @@ + + + + index.theme + + + scalable/reddit-alien.svg + + + scalable/launcher.svg + + + scalable/technic.svg + + + scalable/atlauncher.svg + scalable/atlauncher-placeholder.png + + + scalable/instances/modrinth.svg + + + scalable/proxy.svg + + + scalable/language.svg + + + scalable/java.svg + + + 16x16/star.png + 24x24/star.png + 32x32/star.png + 48x48/star.png + 64x64/star.png + + + 16x16/worlds.png + 22x22/worlds.png + 32x32/worlds.png + 48x48/worlds.png + 64x64/worlds.png + + + 16x16/minecraft.png + 24x24/minecraft.png + 32x32/minecraft.png + 48x48/minecraft.png + 256x256/minecraft.png + + + scalable/bug.svg + 16x16/bug.png + 22x22/bug.png + 32x32/bug.png + 48x48/bug.png + 64x64/bug.png + + + + 16x16/screenshots.png + 22x22/screenshots.png + 32x32/screenshots.png + 48x48/screenshots.png + 64x64/screenshots.png + scalable/screenshots.svg + + scalable/custom-commands.svg + scalable/datapacks.svg + + + 16x16/cat.png + 22x22/cat.png + 24x24/cat.png + 32x32/cat.png + 48x48/cat.png + 64x64/cat.png + + + scalable/centralmods.svg + 16x16/centralmods.png + 22x22/centralmods.png + 32x32/centralmods.png + 48x48/centralmods.png + 64x64/centralmods.png + + + 16x16/copy.png + 22x22/copy.png + 32x32/copy.png + 48x48/copy.png + 64x64/copy.png + + + 16x16/help.png + 22x22/help.png + 32x32/help.png + 48x48/help.png + 64x64/help.png + + + scalable/news.svg + 16x16/news.png + 22x22/news.png + 32x32/news.png + 48x48/news.png + 64x64/news.png + + + 16x16/status-bad.png + 24x24/status-bad.png + 22x22/status-bad.png + 32x32/status-bad.png + 48x48/status-bad.png + 64x64/status-bad.png + + + 16x16/status-good.png + 24x24/status-good.png + 22x22/status-good.png + 32x32/status-good.png + 48x48/status-good.png + 64x64/status-good.png + + + 16x16/status-yellow.png + 24x24/status-yellow.png + 22x22/status-yellow.png + 32x32/status-yellow.png + 48x48/status-yellow.png + 64x64/status-yellow.png + + + 16x16/status-running.png + 24x24/status-running.png + 22x22/status-running.png + 32x32/status-running.png + 48x48/status-running.png + 64x64/status-running.png + scalable/status-running.svg + + + 16x16/loadermods.png + 24x24/loadermods.png + 32x32/loadermods.png + 64x64/loadermods.png + + + 16x16/jarmods.png + 24x24/jarmods.png + 32x32/jarmods.png + 64x64/jarmods.png + + + 16x16/coremods.png + 24x24/coremods.png + 32x32/coremods.png + 64x64/coremods.png + + + 16x16/resourcepacks.png + 24x24/resourcepacks.png + 32x32/resourcepacks.png + 64x64/resourcepacks.png + + + 128x128/shaderpacks.png + + + 16x16/refresh.png + 22x22/refresh.png + 32x32/refresh.png + 48x48/refresh.png + 64x64/refresh.png + + + 16x16/settings.png + 22x22/settings.png + 32x32/settings.png + 48x48/settings.png + 64x64/settings.png + + + 16x16/instance-settings.png + 22x22/instance-settings.png + 32x32/instance-settings.png + 48x48/instance-settings.png + 64x64/instance-settings.png + + + scalable/viewfolder.svg + 16x16/viewfolder.png + 22x22/viewfolder.png + 32x32/viewfolder.png + 48x48/viewfolder.png + 64x64/viewfolder.png + + + 8x8/noaccount.png + 16x16/noaccount.png + 24x24/noaccount.png + 32x32/noaccount.png + 48x48/noaccount.png + + + scalable/accounts.svg + + + 16x16/log.png + 24x24/log.png + 32x32/log.png + 48x48/log.png + 64x64/log.png + + + 128x128/unknown_server.png + + + scalable/screenshot-placeholder.svg + + + scalable/matrix.svg + + + scalable/discord.svg + + + scalable/instances/chicken.svg + scalable/instances/creeper.svg + scalable/instances/enderpearl.svg + scalable/instances/ftb_logo.svg + scalable/instances/flame.svg + scalable/instances/gear.svg + scalable/instances/herobrine.svg + scalable/instances/magitech.svg + scalable/instances/meat.svg + scalable/instances/netherstar.svg + scalable/instances/skeleton.svg + scalable/instances/squarecreeper.svg + scalable/instances/steve.svg + scalable/instances/diamond.svg + scalable/instances/dirt.svg + scalable/instances/grass.svg + scalable/instances/brick.svg + scalable/instances/gold.svg + scalable/instances/iron.svg + scalable/instances/planks.svg + scalable/instances/stone.svg + scalable/instances/tnt.svg + scalable/instances/enderman.svg + scalable/instances/fox.svg + scalable/instances/bee.svg + + + + 32x32/instances/chicken_legacy.png + 128x128/instances/chicken_legacy.png + + 32x32/instances/creeper_legacy.png + 128x128/instances/creeper_legacy.png + + 32x32/instances/enderpearl_legacy.png + 128x128/instances/enderpearl_legacy.png + + 32x32/instances/ftb_glow.png + 128x128/instances/ftb_glow.png + + 32x32/instances/ftb_logo_legacy.png + 128x128/instances/ftb_logo_legacy.png + + 128x128/instances/flame_legacy.png + + 32x32/instances/gear_legacy.png + 128x128/instances/gear_legacy.png + + 32x32/instances/herobrine_legacy.png + 128x128/instances/herobrine_legacy.png + + 32x32/instances/magitech_legacy.png + 128x128/instances/magitech_legacy.png + + 32x32/instances/meat_legacy.png + 128x128/instances/meat_legacy.png + + 32x32/instances/netherstar_legacy.png + 128x128/instances/netherstar_legacy.png + + 32x32/instances/skeleton_legacy.png + 128x128/instances/skeleton_legacy.png + + 32x32/instances/squarecreeper_legacy.png + 128x128/instances/squarecreeper_legacy.png + + 32x32/instances/steve_legacy.png + 128x128/instances/steve_legacy.png + + 32x32/instances/brick_legacy.png + 32x32/instances/diamond_legacy.png + 32x32/instances/dirt_legacy.png + 32x32/instances/gold_legacy.png + 32x32/instances/grass_legacy.png + 32x32/instances/iron_legacy.png + 32x32/instances/planks_legacy.png + 32x32/instances/stone_legacy.png + 32x32/instances/tnt_legacy.png + + 50x50/instances/enderman_legacy.png + + scalable/instances/projtlauncher.svg + scalable/instances/fox_legacy.svg + scalable/instances/bee_legacy.svg + + + scalable/delete.svg + scalable/tag.svg + scalable/rename.svg + scalable/shortcut.svg + scalable/export.svg + scalable/launch.svg + scalable/server.svg + scalable/appearance.svg + scalable/about.svg + scalable/new.svg + scalable/checkupdate.svg + + scalable/instances/quiltmc.svg + scalable/instances/fabricmc.svg + scalable/instances/neoforged.svg + 128x128/instances/forge.png + 128x128/instances/liteloader.png + + + scalable/adoptium.svg + scalable/openj9.svg + scalable/azul.svg + scalable/mojang.svg + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/about.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/about.svg new file mode 100644 index 0000000000..b97c79d897 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/about.svg @@ -0,0 +1,3928 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xmlimage/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/about.svg.license b/archived/projt-launcher/launcher/resources/multimc/scalable/about.svg.license new file mode 100644 index 0000000000..92494ca5c9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/about.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 KDE Community + +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/accounts.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/accounts.svg new file mode 100644 index 0000000000..e6a1328dd3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/accounts.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/adoptium.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/adoptium.svg new file mode 100644 index 0000000000..d48f8b7d90 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/adoptium.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/appearance.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/appearance.svg new file mode 100644 index 0000000000..429670c365 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/appearance.svg @@ -0,0 +1,2440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OK + + + + + + + + + + + + 22% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OK + + + + + + + + + + + + 22% + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher-placeholder.png b/archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher-placeholder.png new file mode 100644 index 0000000000..8b6dedad5f Binary files /dev/null and b/archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher-placeholder.png differ diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher.svg new file mode 100644 index 0000000000..1bb5f35982 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/atlauncher.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/azul.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/azul.svg new file mode 100644 index 0000000000..31c901babb --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/azul.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/bug.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/bug.svg new file mode 100644 index 0000000000..178e3c23c5 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/bug.svg @@ -0,0 +1,387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/centralmods.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/centralmods.svg new file mode 100644 index 0000000000..a8b123d069 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/centralmods.svg @@ -0,0 +1,346 @@ + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg new file mode 100644 index 0000000000..4ab18c929b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg @@ -0,0 +1,1566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg.license b/archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg.license new file mode 100644 index 0000000000..92494ca5c9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/checkupdate.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 KDE Community + +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/custom-commands.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/custom-commands.svg new file mode 100644 index 0000000000..0d502bb1da --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/custom-commands.svg @@ -0,0 +1,4339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/datapacks.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/datapacks.svg new file mode 100644 index 0000000000..5e136b2025 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/datapacks.svg @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/delete.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/delete.svg new file mode 100644 index 0000000000..414cbd5c67 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/delete.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/discord.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/discord.svg new file mode 100644 index 0000000000..e37c3b8421 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/discord.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/export.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/export.svg new file mode 100644 index 0000000000..2605de14e3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/export.svg @@ -0,0 +1,466 @@ + + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee.svg new file mode 100644 index 0000000000..1656a6fc2a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee.svg @@ -0,0 +1,136 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee_legacy.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee_legacy.svg new file mode 100644 index 0000000000..49f216c8fd --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/bee_legacy.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/brick.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/brick.svg new file mode 100644 index 0000000000..f438466c60 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/brick.svg @@ -0,0 +1,67 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/chicken.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/chicken.svg new file mode 100644 index 0000000000..92c8a52847 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/chicken.svg @@ -0,0 +1,130 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/creeper.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/creeper.svg new file mode 100644 index 0000000000..0cba8d7650 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/creeper.svg @@ -0,0 +1,68 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/diamond.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/diamond.svg new file mode 100644 index 0000000000..ad73de0b67 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/diamond.svg @@ -0,0 +1,62 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/dirt.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/dirt.svg new file mode 100644 index 0000000000..29e646245a --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/dirt.svg @@ -0,0 +1,52 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderman.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderman.svg new file mode 100644 index 0000000000..e993b1b497 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderman.svg @@ -0,0 +1,96 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderpearl.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderpearl.svg new file mode 100644 index 0000000000..cc3d57036c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/enderpearl.svg @@ -0,0 +1,95 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fabricmc.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fabricmc.svg new file mode 100644 index 0000000000..7bfc754876 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fabricmc.svg @@ -0,0 +1,71 @@ + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/flame.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/flame.svg new file mode 100644 index 0000000000..336d6af15b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/flame.svg @@ -0,0 +1,49 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox.svg new file mode 100644 index 0000000000..2c84999adc --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox.svg @@ -0,0 +1,151 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox_legacy.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox_legacy.svg new file mode 100644 index 0000000000..fcf16b2fb9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/fox_legacy.svg @@ -0,0 +1,290 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/ftb_logo.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/ftb_logo.svg new file mode 100644 index 0000000000..70905da86c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/ftb_logo.svg @@ -0,0 +1,82 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/gear.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/gear.svg new file mode 100644 index 0000000000..9c0a1759ab --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/gear.svg @@ -0,0 +1,68 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/gold.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/gold.svg new file mode 100644 index 0000000000..3091dfbff3 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/gold.svg @@ -0,0 +1,63 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/grass.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/grass.svg new file mode 100644 index 0000000000..36f022251b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/grass.svg @@ -0,0 +1,84 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/herobrine.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/herobrine.svg new file mode 100644 index 0000000000..cf042db3ec --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/herobrine.svg @@ -0,0 +1,111 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/iron.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/iron.svg new file mode 100644 index 0000000000..f9dcb5a8c2 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/iron.svg @@ -0,0 +1,178 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/magitech.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/magitech.svg new file mode 100644 index 0000000000..e8b33de63f --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/magitech.svg @@ -0,0 +1,85 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/meat.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/meat.svg new file mode 100644 index 0000000000..2b9290fe70 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/meat.svg @@ -0,0 +1,121 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/modrinth.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/modrinth.svg new file mode 100644 index 0000000000..4b420df505 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/modrinth.svg @@ -0,0 +1,92 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/neoforged.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/neoforged.svg new file mode 100644 index 0000000000..706d53a0e6 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/neoforged.svg @@ -0,0 +1,3 @@ + + +Sefa Eyeoglu <contact@scrumplex.net> diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/netherstar.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/netherstar.svg new file mode 100644 index 0000000000..39a33cb779 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/netherstar.svg @@ -0,0 +1,81 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/planks.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/planks.svg new file mode 100644 index 0000000000..4cc664d760 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/planks.svg @@ -0,0 +1,93 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/projtlauncher.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/projtlauncher.svg new file mode 100644 index 0000000000..1f65d4d611 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/projtlauncher.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/quiltmc.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/quiltmc.svg new file mode 100644 index 0000000000..a7aaca53fe --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/quiltmc.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/skeleton.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/skeleton.svg new file mode 100644 index 0000000000..ce2ab89cdf --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/skeleton.svg @@ -0,0 +1,134 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/squarecreeper.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/squarecreeper.svg new file mode 100644 index 0000000000..28fb286653 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/squarecreeper.svg @@ -0,0 +1,81 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/steve.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/steve.svg new file mode 100644 index 0000000000..5dd0e3acbf --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/steve.svg @@ -0,0 +1,154 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/stone.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/stone.svg new file mode 100644 index 0000000000..d7e1c82117 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/stone.svg @@ -0,0 +1,55 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/instances/tnt.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/tnt.svg new file mode 100644 index 0000000000..f1d4f8bc0d --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/instances/tnt.svg @@ -0,0 +1,126 @@ + + + +ProjT Launcher LogoProjT Launcher Logo19/10/2022ProjT LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/Project-Tick/ProjT-LauncherCC BY-SA 4.0ProjT Launcher diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/java.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/java.svg new file mode 100644 index 0000000000..fd15e5c664 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/java.svg @@ -0,0 +1,773 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/language.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/language.svg new file mode 100644 index 0000000000..968e353827 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/language.svg @@ -0,0 +1,109 @@ + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/launch.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/launch.svg new file mode 100644 index 0000000000..321647a0bd --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/launch.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/launcher.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/launcher.svg new file mode 100644 index 0000000000..0bf2cf6db1 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/launcher.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/matrix.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/matrix.svg new file mode 100644 index 0000000000..237c55a297 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/matrix.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/mojang.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/mojang.svg new file mode 100644 index 0000000000..0c1f48d3db --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/mojang.svg @@ -0,0 +1,55 @@ + + Created with Fabric.js 3.6.3 diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/new.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/new.svg new file mode 100644 index 0000000000..8b9f1a8ab5 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/new.svg @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/new.svg.license b/archived/projt-launcher/launcher/resources/multimc/scalable/new.svg.license new file mode 100644 index 0000000000..92494ca5c9 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/new.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 KDE Community + +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/news.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/news.svg new file mode 100644 index 0000000000..67a370dfda --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/news.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, viverra id interdum in, molestie non sem. Morbi leo orci, gravida auctor tempor vel, varius et enim. Nulla sem enim, ultricies vel laoreet ac, semper vel mauris. Ut adipiscing sapien sed leo pretium id vulputate erat gravida. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras tempor leo sit amet velit molestie commodo eget tincidunt leo. Cras dictum metus non ante pulvinar pellentesque. Morbi id elit ullamcorper mi vulputate lobortis. Cras ac vehicula felis. Phasellus dictum, tellus at molestie pellentesque, purus purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/openj9.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/openj9.svg new file mode 100644 index 0000000000..53b11be11e --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/openj9.svg @@ -0,0 +1,17 @@ + + + + Logo + Created with Sketch. + + + + \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/proxy.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/proxy.svg new file mode 100644 index 0000000000..55ee6f937b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/proxy.svg @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/reddit-alien.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/reddit-alien.svg new file mode 100644 index 0000000000..46061a5627 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/reddit-alien.svg @@ -0,0 +1,189 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/rename.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/rename.svg new file mode 100644 index 0000000000..a585e264b0 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/rename.svg @@ -0,0 +1,437 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/screenshot-placeholder.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/screenshot-placeholder.svg new file mode 100644 index 0000000000..a7a2a3d67b --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/screenshot-placeholder.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/screenshots.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/screenshots.svg new file mode 100644 index 0000000000..a3d4d8e254 --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/screenshots.svg @@ -0,0 +1,1231 @@ + + + + + Golden Picture Frame + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Open Clip Art Library + + + Golden Picture Frame + 2012-05-24T10:08:07 + Golden picture frame, Landscape + http://openclipart.org/detail/170182/golden-picture-frame-by-tasper + + + tasper + + + + + clip art + clipart + frame + golden + landscape + photo + picture + + + + + edited by Paul Sherman + + + + + + + + + + + diff --git a/archived/projt-launcher/launcher/resources/multimc/scalable/server.svg b/archived/projt-launcher/launcher/resources/multimc/scalable/server.svg new file mode 100644 index 0000000000..c6a957b36c --- /dev/null +++ b/archived/projt-launcher/launcher/resources/multimc/scalable/server.svg @@ -0,0 +1,9764 @@ + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xml + + + + eJzsvXd+6kzWPzgb0B5wwAkbSwIEOCvhnHGOGLCNjQETnu6n/5j1zD5mY3MqSVVCEiL0r/t95159 +zLWRVPFbp06u+NTZ5Ypeab5VV1JJOSbF42a7Wuo222sx/G1sv17vdbpt9NXCxWJMUZMyPKTv517o +g9fVdqfWbKzhW/hmAb29YJX+qlVi17WPRrNRW4wtXO/vnpye7Mcs/XrfWoTHirVuvQoPfjcbv71q +u9lWlWSptsiaAGVapS48oOZW5PyKKstaTEmtyVl4wGj2GpVa48No/nMttpLS4CcbU7QM/KTh9l7t +otrxPJNJZtCTSfwM/kwlM/Cs1Sz3fqqN7lm7Wa52Omaz3mx31mLm36VG7Lj0AXdKsbtqvd78R8yo +l8rfEvQ981Ko1avQzZ9SN5ZDfdb3FfXF6NXqlZPez1sV+q/KCvo69YJLvOpAUVAq+h19nX3Z/4Fv +LqvdLjQR6kPjdrFrmDDWzR/8GHyHr4WHi+pHDaag+bRIS2w3Wz+l9jd6DfUKfShqDn9mUafQQ8Xq +T6sO44e7j3uMOuz+Rp+CfpABUtU0fOS1WCqjxtIZ2nh3dKp/1ar/WIudNBtVMgR6u3tZ+1cVzXsO +/ZBvL3r1avuqUetC4zT0VZ4MwHGzUq3Ds867hXoJ9xtfivtJHiiW2h/VLsxis97rYnTlZHoLBvio +9HcVTZJCKjhtVRvF5jVu34qWlqH5eShMSUPDcko2pmq4aC2WTdF6FPwFbQx6Hb3MSkUAO4PJOW3X +PmqNNdqm7Mtuu1ZxJyylAoToJ254Msf95NkPaSF0ttutNmiLASrmMTf1cvL4Euq0GxWz+YNGu4Mg +DpPeAFDWmx/knvM7vgOv91qk/fjvF5iYs3atgcqUTvCd3MtZvQe3dtvNXmu/8d6UFsiSPit1PwHM +1UalA+sSPbRSa9Dl/t5Dixmt1wL5LQZrqPRTK39WYXG2FkMLLrZLZag/dvr2VS13oYyLWrlULzfh +N5j/Xr0Za5MvBhd1WUYj1o4Z7V7nM1ZsNutcU8+qjQZaktC2d9LgKvxKegCPXXbbGLTNWMt9cCab +s3Xtv7hiwEgb5gbfRMOP/sRD+F4HuhRlyP7+eWvWa50f1JIa/t1tGfl7cCFnuPeN0wYBid8gkAed +HrbIG/7dL7VR92X1v7hioEWDKz1zy+0CLa5hTP9PrNYs1eu1j3ap9Vkr+9fsFFqmj75DtQNQXi6h ++jPpsPpRX99rjQqsrcterYtKwjtorNz8aTU7sM2yCiitqNZLb8126V94lXVnspqih1ZAOmhV36FU +dx2xbu03/oK9pNwWuuJ+CRNSBzSVIgC1XmqU2jH8ff+E1brdXrvEiGYJPeD5yqkh8wJUnafbKytS +NmY0OKq+2y5VajDUwCVdNeD1aiX2Qb+KKYuS35ewGaVjRkV6kDakgvjPxpdVMAsGXHohX8jBpeEr +DZcKl1KQ4SEbfiy4TNuAS7fzds7O2lnJ1uyMnbZTtmortmwV4Bn0z4RLt/JWDq6spVkZK22lLNVS +4JLNgmmjB0wDrjxcObiypmZmzLRkpk3VVEzZlKFJtmHBM4ahG3m4ckYWLs3IGGkjZahwKYasF3Rb +t3RTh4f0vJ7Ts7qmZ+BK6yld1RVJl/PQr7ydt6AqVI6ez+dz+WxegysNVyqvwqXkZei7jRoMzdFz ++Rz6l81puQxc6Vwqp+YUuGQpW8iirltZE5oD1WXz2VwW/dOymWwarlRWzSpwyZqtQd81U4NGazrU +B49ompaBK62lNBUuRZMlTc7AIGag+xkDtTyTR1Vmsui5TCaThgtYMLhkNCdpOw2DmTbTMAppHXUA +1ZnW0GPpdDqVTklpFS45VYALZiZlpWC4UjAcqXwK+pGCBqa0FJSZSsHTKnyqcMloslWYRxXmSYVJ +QAOswhCqeQkGCLoP7c2iZqDS0Rsq+qeoilIA1hamH02uYuILZkbR4crjC42Ghq8MXCklJcGHii8Z +XXIBXza+THoZ+NKdK0evvJxll7SFIK3CmwSeCJwImDlAZRp6ImNEIiTmAItZjEHURIQ/0wTMYNxl +zBQBHQacLQHmDMCJDnjTMNIQ1hDOChhnOsUZQ5kKjVMwwEyAGIJXDgNMy8M8wuyoEkYYoAt+EL4M +D740jC8VI0wG7rSAwYXgpQPAELgQvFxoqRLCFqCrAPhCCEPYMgBdOYwwDSMsjRGmEIQBwAoYYrAO +McgMDLI8BhnATMpkOJylYJJk/IPwZgHeENZMwJoBWMthvOUcvCHEYbilFRdyEmDOTpkUdwbgTsRe +FrBHEJhOpRj+ABcyQiH82IBEy0EiDL9EgMjhEK2ejAeNKUCUqsrwKQMiCxiX8NmHTfiRKEB1ClC0 +thlMswBSTUljsKbx/ymK15Si4P/JPwe78GlLADYEYouC2MIXgbJFwWxiIJPf83AxaOfdC0MaDSUM +pnftop88Hkf2kxV+NO6HjDD5LSvRXzP4JpsE9xv3YY1ODPvJOr8jwpLBn/C/hClHmlIQ8X+Xpri0 +RcE/aH4VPL+E0hSc/y2JTjaadBNPugXTTsgQ+STfMKJkABJcPGTxZw7+yuK/AB0SJlUEJBoAhAAF +N4YCJu0ARoANJWcMNg54JIwdy6FuJkCHUDnyk3eoXY5+Zh3Kx6ifCyz4lAR8pTG6GFVUMM5kB2UK +wphAJxnQCMww4CQHbwRrpoM1A/+Wx//nPYjL4R90IZqSc4mrrEmyhq+sjGgB+T2Nf8e0gV7o9xS+ +3G9Tzl24MKTx7Bl4xrJ0StJ0/5Cd4S4442vScSUjmcMLlI0i3kckOnZkzFJ01GQyXmRFcjuKRReh +6QwGPwi4+5Lbb/xDLrGrpGvkUvGPeyneS0INmeQ/abLF/SlwjGIQpOF/BcMgjVdHFi8lHaMMrcgC +RqOK13YGL/4cJhIGJSQ2ojOU/iD6hElWDtM1A1M7G5FKvC2qmKhmMIXO4Y3AgM3VwptsAe+8KlDf +NOzHGuYF0R4Nu7UEmzbavgsYxQre2tOw0Wt418/B/q8DJ2ACT4B4gwLgHfELKeAcEOepAS+BOAod +cxcm8Bm2pBXwElGAxKYwP5IBjjYL/Amw05hbMTHnYmcLiJPBRDkFvE0GczlZ4HeALwLeB3FAiM+2 +JcQUwaUAXUhhZjwDfBMqDfFQwGYBP4W4Kgu4qwLeKxXEB8B2mMbcl4bYb+DF8rqO+TJTAgbNAoEA +mDVY6YhAq5iBSyOeB9g5JEDksDChG+ifCcyeBVwfSD9AMNHMoK0G8YNp4Auh1xIwiFlgE5GYghhG +w0T/kPBiAxtZwORXwTsXmg/EmSMmS0OjgIUf1HhoEaoFC0WWBNIRkqRA6sLUCe0viOtGuz3MFZoJ +xNUhCQNzsjngaPMgcOlY8DKAxzWxIGbZNuZ6C5JdINtCAVNRsqkWMCeGZTh0ZfClOVfWuXLsIlSa +YM9Fn8Khz8WfHwJdDBIUIgzmJcw0mj44ZEjUHCQyLFoUiwyNBI8awaNEIWkwSMJVwIgkmExRTBJU +5igqOVx6kJmWOHDmKDgdeKKBwegk+ExhfLoI5TFqYowCSiUKUwXD1AWqhoHqQtXASEVYtTFWRbSm +MVoxXiXUIAxYBlkTQ5aBth+2GLgUtzxyDYJcCUGIA68LXx7A/RDmQEwxTFEsURi7QHah7ILZC2ce +0DykbQxpdAmwZsBm0HbBzeDtBbmGIY1xgyiZS8sQdBzwcCSNAciFkAsiBiNTwjhiSJKxTgBhCaMJ +6woInFxqxyDFQGU7RA/BCouHjPZlsFLCiy6GL4owhxIyWohAxsFMcpDGY42gjeJNQByPOYI6Ricp +pZQo8hj2RPTx+Et5MCii0MGhZJocGeXR6OLRJagEkS4mCSoFXEoOND3gpESWxyePUB6jDKUYpxIF +KoOqCFYRrrZdEPDaj1jArBQKW82XNnsoNKXS6Vj8xWhLclJWs8gqB7+A1J5Po1+UDIBPAi4nmQcu +G+gv/ALEGLHy7Bv6+obEcfrsyjtKFna56hfDuUznomNLOi1RjkehMkyKXhl8MQEoS4UkInsblBsy +sXhOJolwRcAXSZg1YgJbhopzRM7LY32A4exWsF9h1QESMZGomeL2LCIEw74lwUJDsrSzccFksI3L +3bpylH1yd64C3rkUrG1IYTUJ5aEkWO46VqeITBRjo9DGRZiofN9+xe9WDgcl0a2KkBdGWggD5e5P +JtmbOL6J7UqEZhh0NwI6IdF9KI2JA9AEwrnmfS7dc+U54RFdrk7OVWxQ+VOiwij6kwqoVAxjAqzK +AUOhQluawiNNVS0uRDSJis9oCHQHLwwtBhYTkVTuyOhIhMSipIo5atUDm4yENUZIN4A0BkhzgLQI +poMfE6shCIYYz82UGCmMI4akHOGAJMwCESzxeGKsUFpAFGWHBEzx/FA6k5IwrLIYWHnKEpkeZKlY +aexl0NG2RrBlUXQh9bMqOeAi8NLJ1Kd9rpQosAsCPS//as43TB2AdQQZR3DWHEVCltPXMhmbydyG +QFIYrgimbInCi0MTU7LRS8F4IsIVjylGdIhawMGVRBXPSH2Qo5o+lwblqfrGcFWDAj2yKMIKLsIk +B2IqViRlKMzQlcW6yayDtDzVUxkcvTKofstimJOITgyDTk5RmkdVbWkq+Lnw0wB+WU4HSPSCTNOK +tmBLopC0qD62wOTVif77U+B/psA+TRPVQOE1K3GLGS9UYiPJ9V1eNiPLsSAuY+IyK4x9wfyMhO2C +6CIrlDJAlB2yqKXQpEwTYaDy1DSD7YaU48pg42HaTknYgqhiK6JC7TYFXA6zJiIGT8c2xTy2KWYx +P5jBlkViW6TWRWJflKiJ0cRyjy6YGbGhEa4U5lpdWyOxNvrbG9MSNgMpjsnRNTrq2BzEDI9pZnjE +1M7f7ogNQ8QsRMyOxPBowt5ODENZanhkZkeZGoVEiyOxNxJro6wVJLwDmNTUmNOoeUzzuTJ9V1q4 +UuJ0UzswviQ26QHTblEO2eibfP/pz0gEAfjqh4AIAi8M/ICQkjxYKOAGETD0w4HYnRkgUhgQIiQs +CWPCRUUOo4LiwrVEC8AwKTCIRVq0SauIK3Ot0gQfuoMPgpA0FgsxQBzLtMlBJOdABEAiOSihOAla +9UFrPxsKEQoLaRA2HAHICw1bgIZDF6QAdAj4oIKaFyBeiGCQSB6UiDgRkYKxwkPFDywSRosXL17E +8JgRCYmB1TYcMZEwbjKOXdklKSJ2XPQQwuLihxAXB0ASpTAOhlwR8UFSkMNhOi+r8D+S/vAvShZE +QuKgqGDhEH5Jx9RYKpdMA6MUy6lJFUQOVMalFH8Zqwyj4wiqMSaoxpigil8nYmmMCaruN2pM05KI +yY3l8tAAOccaNLkCJ9K6fDopw/KbXOtYgbh1moZKQ6ObzaVTqFQV3svkcal54LfRLRkIBZqVdA6e +Rg9rUFxGjgHj7DZr7JKgPUALmZsSc0SK6pvk75yEvZNU5p1E2RvXosLsKTYWJVVqSQ== + + + ce0olteGIhpQJMpUI10AYZ6Z3toWrCaOxYRTVLvmkqwjjVlSxqZ66bRgIWHCfYEzjvQbRphZxFUY +ZiRfy4ireFY55aDGmUbygmLQVQsqEmchSTuK52CFoKgMFNWBWCEoOfpAVyPoVUb7q6NVRx1IFIIZ +rA7MSdBmoom2qe0khVV8WazSQ8q8LRcKrnktzMAmmthccLjqJBDamD5A8zWz+RnZHLD4GNlyUghi +tD7zWsFjXmNKac6WITkI8jew6RRHpqCAVnhjhmhokyieTKZ15owZqqBydhXOokGDIoupmyUBWaJR +I88ZNVzzG0GTq1jmlMoAHGDSEVpSGBk5QCQCRQFDIo11vflBUBDBwNtbRYurwVtcJYFgiGAgMnmB +0wAxSIiWV5sHheSiQgCFFxYiMEKUhhIzdwkGWJ7E8PpD0d7ltXhhoiP5GCp4q1deMFK4YAmEi8Th +pc9AEWABE+1fHAECqKhIvMpQ4ywiK0BOMBlBpAPbX//3qDD85HYmrhNwZ6naDIZR6kO4S+w4Qsft +ggTSOlYz2RjIbPtD8DWRax5SeyNShlSTOlFHAkxTeK/LAzAtgKQCYETaa6K5trFWEW1qOcCXmbUB +TCkAUTanS4AZG1qbAmxkAQ4mAECBaUfqZh0muAA0IA0zi+bVhgWv4hnNw2JH9F+ltD8Pu4hl2UCx +UxKsCWSE0YETt7E1JQXkExlCdGQmKhBtFvZeJp7LxG85i4WWNJZLmM8ycQll3sp+rsomlhRARpA8 +fsrMS9mk8qLjoUxlRK97ck5wTQaWXuI8k3XOM9lRD3DKAV41kOb0AoJWQApVCviqBLwaAeoamqKO +x9gr1KJux3nqcpyhbqAKdv+0qauxjl0/iZsxcfkk7p421n8bxL9Toh6Eaaw5V7Cjn01dOHWsD81i +h7w01p0q2O/Lxsp84utFfLwyWH+L3bkkbEywsIZYx1rkLNY3p7HzlUIdcf5A4Q8U/kDhDxR4KDgq +ENkJKFWwqJlBfzCh2O8eFrthQHNqCoVuZuQ8koyz6O8MDUuVfcTryG+MJ0arvmK0isTonCBGM3aZ +l5pEp0TCQvQzEBmOhXDEaJnnvxSZiOkq/clQxjxDjYE5+pN3/I2J661FbXoOMyPhjy3ksTDc4o2w +dK2shJauSxZsrCxkoU4s2CnraDvTjlJTcVSYthD0pFMtZVZClUPJiutOEeqo6QoLvKQQIDziFlPL +JnFuznAijUYN40zOZT/MO50aK4kLBZpRZ3gjkrEoRAy1kDffuGp7qo91rTZ+NhuPxUYcSD/pmboD +pgS1C6d4Ic6LKerXr1FXEWbsNwS3RlfAg/lyBkcIJBMJcyBZxrU6WmPXwKDTMfVamxzjgthhxwuN +Sf28zG9QXT6GFmHkMzTcw1VRIJMv60jfbiHalUijHU21RafZY/ISG0hlSaxuII2xMIW3qWmZVOzE +MaEYOfSUo+kG3IgFYkkUSaFMV8ErKniPy3SQXwv1BKaOLZLjBMwUELajgGDevxoVLl2/X+bzSxwn +M46zWt7UJeYtSbpbwEuXLd+0s4DdRYwXMtoaAxxcdbyZUjRLjjKIQNpVBjliPtqmsciVwls3wkMe +i16GK9Q70rxGeAAYSHcyxmYDRC5AwmzAGEyAlweQGBNAoOJvW9W5lagJ5g8+ptNdkw6zJDkrQAzr +JOsgLwTepbD+TLCeYQWKENcpseg7cTsx6Wbl3UzSdNNi25Z3I8mz+FlCRCVq3/KjoQbdz1wKKtJP +au+m5JySH4nRH5cTgkVvca6WiuC17vVZJ86+xJuSd1Xn3dQ1iTpL5ql3JPGLtB1PSOYBydweXVdH +5taYx0On4x0Z2iURh030PvuHhxsmzMAkwvWeJQTCVUih2UcEAuGB6MIReSCKyixVTxJ9U8FQJKqV +JBpJosw2qQJboSprDesocthf1SK6CqyaJC6pWDmJdRYmmlbiAI2GBmku0NT++9RVTBFK+DqqHKJc +nepwCYw7wDpwygukKGeXdfSdhLOjPkcp7Oim/R/Ss4kqMNUJqxIVYUwNZniU/inJE1QDfA/hAYS4 +ZSc80A0X9YQuc8FnbogoF7wsCYGhxKcs7YSbYe9GLijPjb9zw8tyjmscFlEk6m7HQsZkKrg6IeMs +qNcNGs85xFkIG3dItCXEjWeJuCZxYeMKFzhuOiSbEW1GtgnhZqSbEW9Gvh0pjtFwoOKElRhSAB4k +/0ocpxVB/B0s/Ur9+94A+VCRk7IWIiOS+1hOVIiQh77AtluZ5P1hht88sqw676mpZCbrSowjvIvr +TEFPMug9hf6C39Pw86gAYmym76WySS3j1jnCu7jONOzZKFdSElmKszn6Xob1IIva7LQ1gzIeOVUO +/yqpEVmg0TioMPOKxl6j1vMsar/TUE1NZhSuxqFfxTUC2YFGcXZx8hqZBnhPmI+sknQhMsKrRMvA +zO0ZfjYU5iKQZvb5LH6QvJ3LJ1PchI5eBLQAu3ZgJKCmK0pWw9DAzh2kHBg/gJ0ziV61x+gFjKcF +yftqQfJ/nAn+OBP8cSb440zwx5ngjzPBH2eCP84Ef5wJ/pgN/1iQ/0DhDxT+QOG/3Zkgm0+qwAQN +6VDA3hpLnPb1KVCFvKGY13CZZq9LAe9QQNgIMbtRn5gUZC7nJaRUqLHcIxpJnKlYlIxEkTrAxsrM +m268vSSYJrmAe16MIKIEi7ZkokSORjlzWmtOwuRzQjmChSQkhzLdoGNnCFOOgOEOpc5kTk7upGMq +OZkAveKn5cgargiac8QN0x1o0ZgpOePtCqN4yEXHEAUvGtVxDeFDyzMBTiI550dn2dkkvB5NGh5u +0U/iP2IzDpZLOKE4MeB8HHiKxoCnURS46EKRddT6efp7znGmQHTCdBwrLOpYQSLC0Y+JEtVJNDkg +l2USh4jL2Ab7Z/PgNg9+M3VHhB8TZ1S4cXFHhh8bZ3QkboAcjx9hjNxRcsfJHSl3rOhoSZw7hOsM +4Ubr8QZqPsIzMPRX8on9xUPY75LU75TEuyUxxyRFcEzyuCZJTgwli5/MOHGTYlBtxKhqyYmQDA6l +9Q+NDAiMdAfYExnJG6kHWnl4bJleM73LmNDky6aEORPCm7h5cVWsIgkzoKUdFsVjPZNwFlzCqWSF +LLiKkHSZGc90mrKV5VzN0CyqKcrAKJJv3mXRgskSL7v2S9eCSWyYtmPBpJlKiRmTT3LL0tw6OTRZ +NmYu8ajq6hPcWF7JSfTjZmdmuWDEHDH85U0lxHKBoLzNPrmHhAuvEMo4MtaRMY/MDSVNGEjKQpL5 +MfD+laespGvfRPtcSmIzhFPrsgTFrpGTJSh20xO780RS2LrJifF0SThZLUkiowszRhLRujbnDM03 +y+WadbYNZnuGT8kxQVtchtm8M4M65hLYLGZpPlmNMhIatU6zC2qSuIl1U6Vw2Yv51CpcMieWVdZN +ZGyR/KmSLwBMmtnYmykqKLUQl3zVPxeRNyEy2Vmd3dZyssWyDdnmhpFt14ZjymefLEdvTnIS9TI+ +gM/+zBaHu0jY3yxXb9r5pJl7mb9mQOJePoEvTU1bcLxh+xL5kpRKbuJod7D5BNLst7wz7Gywxaw7 +9C+JS8nD1hn6nSkJyd+acGW4dL8Zmv3HYeUk+rU3eVBayAPMLm8qIf4eyxPsm5tITCQsM3dE28uv +c+y6f4Ysrc+TUMiR5WbJ8qQUFfwJFWqDYB6F/dlEcQZ5ZrvyZmUMziVq+6RjJKnvYBeTfNOJirkY +xVR3bqI7N80dl+RO4nIxBmdjdPIxBqS341IySlxaCP+8jJm+DBP9qe64lHfBySv4fKWR0sUOMhtw +GTglx8iU9jEy+WXf5HMfOklj3ayHkpB8k5vswOn2y77JzbnkTLoz7T45DvszHIo5DnPu/Et9OQ69 +WQ7dPIcOFDgw8HBQWG4gb6pOLzCCIOILGP8UNt48n84sBduS/WbLnS9PtlSpb85Gz1KJ50/iFi0/ +g3QOQzJV+s8k9b8UJlPIWNk/n6Ezahek4EmNPKfCDEqBKz9s9QekvoxKFfgElzmPy0eq39iFs7oF +uHxgpYqjpJJEpw/qFe6qUFxrrZ8BX4xmwCoqiUsQ6ef00a+j6t/1BBWV1Lfl+bnOB2SEZEE6pke9 +ojqqFNcMzjvN8LnMHXWVRNlezfE25QZV8JIQdYC8WZzXBSqSr7ZKzC2tePxsOF8bZ+iJ170z+Brn +d2NiRbaQQzE4hWKKpkPU6IEBeaoT5cOdTJrIsOCMp+KwiIo7qhJVnbIf5nbiehuYdKgtpnOiyQhV +OtwqDXwhY56VBEVh3pFNDDoFThwMFWgKNGmqmHkwg3+wOCThtINZmm4wz6kU2aebBvNPzkH2zz9L +n+phe4UkfWPqCEUVoTS+jlBUEUrj6whFFeFIYSosq12G04vJjkbMFOIr+rSGVPllCkEVKErQiano +1xnmhZETx67/gDJPukBvQEte0LSK48iPJD+WeDQl31SBeTEPXFAauCxFVkbUJXKaRKZv5TWumie8 +j6oPJTqE/rEpGVFtKATHBaRilAbkYtQ9Wffc7Gl87jTLTcgo+Sbew6n3uLyMIXnTfGx5uXRSUzLp +EGsfe4I4EzMP3JSGvJGRKyz5gk+ulUGJuJCjclZVMhrKAYbO7M3CV30pzyZU3lgZz6CPeS2ZTuez +4yc748r6X5XnLOVnS00tYnvt/1Fb6mAulTBNPm6mfrZU0T05jE/14VIDbamR+FTGqSK5nepjJC68 +k6limIxO5HP/0E4d+4Wa5PQWx2E4hYwdJACOD35joW8Kje3CQW804o3EvDkRb1y8G411k5wwt7wb +4Iaj2/jINsonBJmRGTR0qgz0NyOn/c3IjnzD4ceVczTBjGzwPCE9rQ0DSqKpp7Ncsmn+5BcSF8vQ +5XqtuvZ6gjPCmKtimvMc9g/sP4OIJDln1ntXOnJxxyJj4QEJh8YyN1XRKz6Dd0aCPbRfYuzhkxgJ +9jIYfVmMPBNHD9pEHUiCB4lTMoGdhYFXAMylsAtiRowcxDsXczDVaNygTTyQJRisDHZCzgGyTLSF +wlaqAp7QJuuJFXQt6ETGYKwiU8VmKTx4uzkDCZM78KfknEBIcUO0zjR/usJhiAkihOxkHGsCM4tT +gU/CufiZfdzgUOZKgcwkbjvSoOwRUlIUg2mUk59ILBrNmq45oXosdzo71U93sqi7fg/sHEDLEWmw +PabAfK6pfGMzGYceDqE4Cf5l53gjmR4boTp/KTTAjf6qOMIRS9HObEIp55wA8j/zwOi/j38kSqaJ +YYkcLaBRWUv7Pxh9+b+zQMX5Hx++x8tcKmdu4M0VfecZcAss51hMsPlENG8xtxTLsYwWOK0JW2Ru +Cg83soT6oUg0+ijrHHXhZvNAy8r+44L4xwXRxxt1PAeadHQHGhcghegONC5W9AEONH4CccbfgcYn +wYOLHwFBfvnzOaFY8knvoHFoUvsisHUOUzyqqPOIxHmP5DlsZTzeI2LkdY7DmIMygg== + + + MwkDjc+a4WIt5cRc9/kO6Q4gxIRGbhaK/pRGXC4KifMbygh+Q+JpDJaP3C9K/hQ4EpeaQkxOIWbH +8fcY8kmkLnkUAf2qgP5E6u75DGIifow0SVAJuAlF3IT8LCW/izt2ord7ZLyLP1uiKUYIIWO6GTfR +iKuhoefIU8rGzvp2UUlP/JYooWMHfzunymOIeqLY/7ORvKO4RY8uYmf9ROwsErEdd2VL4Sx07umG +Kc5a51rsmNVO42x3Hgue5DkEMdCKF2iH9RzrKdniuZ7MNuefPYUZ1sTcKXz2FEOi8qV7cbbaoH/E +hs4OFHSPFGRXSjhckIhDxD8i68QBk3hNNxbYkLBZ3XPcIDWw84HBTlCwc+ggHxHMbLjYncKm5lvX +nYIYbYm5Fj/HHclK5p7MdQbHg2VxTJiOpS98BCsR6wfZzLM4mQuZpv9FIYPB/1Rq1srSzDI28CZU +I6VhBtWk9tKUY9rzC3v+/5UU4znxW/HV7LkpeogMkKXaPZK2RibHlJIML8xFNUUzvajUXZPkeylg +ZtZ23FaJe6RB3VcZEwKXhP1Ys6REypEQn9YUzQpDuRPqQUk8XG3MqRBexSQLFJ+EiI/yJYwL8Xtl +jpXM+zVNzypTaRIZhfp4FRx/WLIgTcrdwCU5zrHE8dJ1kdXogXtpwQGTOMv+EYoiC0W8LwSldu6R +OezQHNty/5nc6TniCTp5uhWyjVGTHD9x/jCdlLPfAgUm2YH8TtpyDlbysfCIAgN/rhJm6CSfY5WY +GY2OLM2BhxZVaEKGkHQMQbkYgnJTcgFHJDWm/1He/ht+hC1f8uz5rjrZSZfGT3bwhh/xnycwQfJI +F312RM7+Kh7/5R7ulHfclLIkfSo744k/6ck97ck98Yk/98k9+8m5yHj3u7BxnozceVTek6jcp3Xe +9in5GT8F4zETmMWYiQy3ElLuSrBkliVUjJ8wKeuiO3EUxDRKpCMmITEpiRw6RVwFsNIwRTzSqe5B +x07nyNFcxZ7RBvYW17DDsoJ9jk3sGqwBWVb/PVvhv6dAkg7Xxu7weadLKnKppp3KC67JKSG/muPm +/m9r38QLpBwBC8wIuORhL4n9ohZGvmz+ksQ/B1xW2EVOah/e+y3UrChF8H/LCllj+42KOd6oKPlY +FXG6WJaCiLMppgWDomNOZFk0/4tk92SOWcU1FdnwqbyOznNjjg1ZIW9dxOdJ2ShkGQUxZ7PMXYEm +KCPnxyVzJPUcTf7GVTLkiyNnMxtzFCPoMl5Omo2zdq3RrTU+pJOW8yj//cpK+At5ckdvdy+7f9er +HWn1sNH8RwP/EVuTFh6s6nupV+8+LcZWT0o/1diytHpZ+2nVq+wROXbq0dPcluCLiwjx6be6JLt/ +/A1/HMAvX/DVP2Lp2HHs4UmOVeDb2wsJl1uRVs9K0PzYuhRbhYbB/7gL0Ge3AwNH4axUr3a7Vdzo +s7cIzVx4uID3Ot1282kRd/L2XxL/nXRWHsPRBEZr4aTX/KsZK5d+WrVmoxorN+vNdjWm4JLPDKdL +rOXDzOgRNJKfxr4Rww+MAh1FJrcu//55a9ZRMf8X/RoK8nwZUDhtq9Us934A21apW4IFs8r+BnSh +v2rlLgxLqf03+fv2+OikWan63lyPLfzzp96A2yulbrdde+t1q51FAC08qrfbpf8zRUygfO6p8met +XmlXG+QZNba6DyPj3EUf3b9bVXJ3Ya7Refmr1O6sw3q9hNIbH+Kjf5XqPfYs+r4T8BzSgpLHaEs6 +wl//Q0enAasrwsDUm+XvaiXKyLAnl/+z/XqrNSrQUCVC3wAcl9XuCe7E4P7xT09o+sccCSV0JCL1 +v1Z6q1ejAH/grP5PWuhrf0Ve6ujR/zCiUffKvU63+fOfpWT/PhyudUqIjUJ7HiyxqHD8t68LaMt/ +UVP+N6zSzvs//ot34//RG7ocW72oluohgxppQP/T1G5gN/6O0o2//9PdWFW0TApEJwVknf7n+P50 +6rVydd+K0ivn0f9s3xQ1N2iSPqu1j88oxNN58r++S/+oVbqfUXpEH/wv2FcxXv7TOxhuxP/wvesP +bf1voq1/6E9U+vMf7FBs1Wg2Q7rz1uyCQHNUfe+etmsftUaUnvW/899CZC+bvXa5ajR7jcp/XHoB +Hvs/3YSfardUAWFu3Hbkx2zHTIVqT6Ogi3tYQLC+ryixs3a1U23/VY0Vq//sxuxKrVt6q9Vr3b8d +yoobip91VLYgtPVasaNS46NX+qjGzpqtXqu/cDn2XgfgVhvVdqzFqmn+VW23kH64E/5CuV5rxcpN +JD3/M9aufsD67jhN8n2j2evWa41qDOnrv6sRH+5Cr2lDZK6v/KPtUqdbba/8VS13m+3YW6leapQZ +M+yhPCmNEbtWqVLxtOCn1Pn29LnTanY9T5XqNdr0LCNElVYtSb5K02/KzXrbmV59P6b3us3YBW5n +7V8MIw/H1UqtFGvXOs1671/I5vDEocVvQgtOnzEqujFXJygLoGGAin03muVvGMvYR7vJIBDwKGpG +qVuNgcyP9Zh0KDLO3q3v52PH1c6n05ESGtrYRRW1H/1K3kjJwhunvW4L6g9/h2tSPtYqtaCHndpP +r15yH1Gdyc/Huu1So9MqwWot/w09q1Xg6X9VRQqCniu1u2/NUrtCbDnkPorcyAU+E1Oh/2wVD3z0 +o11lBGPgs22modYymVQm+EmFa8DAR7kGDHzWacACOrsK/Yspapb+ygHv8nr3xWw3W0bzn7d39A3g +EpJyyIM37s4PpZMnY7L/K9e16j+GKXuP45McnHzr+2elNqAfQNU5swomECIAC08ZGWCER28+a+XP +s3bzvVavHlb/5igLQazw8GXvDRZZoQm08AIh0XkBjbXmV7z981at9BXv22jUYljEHQfg66gN5Wa7 +Uq30067Y6kmzK9xW+CXTaLp0O1ZrYLrc7NS6kUmNGgMqFUJe+F0AP2si8m9S8n/Bk/++R08pKb8M +ovvCU0WH4GddOJNHCA1ZvSaE3uAJvUCmyNPMVh8rNSqUboVRKvLSEWqC3iYv4R3X+5LiadSpZ7+M +Qtbts8uog00ejjba5NmBwy0+5j/e5JnIA04eH3bEyVtDDTltu2fMV40aWyb6pbm/n8tYsFQqyE8i +nt4+nXtIbNxszq+XbpcPUnOnK8ZOe/fnc+2jMXVQmFpemDdrpWRnVrvas7WZtZ2r3a3j9Pba0eP8 +8U67V84WbPU4F1fS6RlZ7lhf1seyPLuz/pxc2tlYbnV2OofqqhTfWT+aarOHDrrGx9750c5Gunpp +1ja3ylYyOf/RV9VR5Q7qy1qF+Fr2frdrfT0Z6fuVZf2nedTR9y+7n4ktbaZXsNKzN8ZXff5Gilvv +8sGbb2Gz2fx79vr84VEvmsnr4Er559aedja+C087a53kT8JajvcKC7uVdymOB6vw+nLas96fbrJG +fad+u/ZufHbNz+y9IgzH65xVVo5+dza2529IOdDkjvn88dyE3+Z+rf3K/pSxkvua1S9XZhqkDbel +Sk+K578WEmW7nDlfMD/TL+sbejw1lzBOll8TO+b8VcGs9pa2rg9mPtfL5dI3+q2WsN+PPknNirxa +yrZrs69rteeDilGPb8+vtBOPPf3ocu4XtX9xZ/3gMyXFtfXrpx29UZ7/SWwer69mfx43a9nsauc9 +pbfL+0rie01xSixbB51rGLbsfDV7k5IrazVztQTzqxxvLqwsV4169uyH9ODuKL5j7m/M3NjL+UwH +5mX/QZvZyprN58TGdeVhTX2becLFbjXi0KEtbWkGTcmDdqOdN9A4bRnfi9oKheZ15UhWnmaOrdXS +xlxhKnHfRrVo6MYzLgU/IsXlt+n9NP49sVXYoL9t3NiH5HFz2X4lhal36j5A91ZObG3Zy6q1/bFJ +y7nZ3FivfJ0845l0GgzlnRoZWgs8ZBw4DXhyG6AsbF6gh6pp/F1myrBe8FBb1c52WrvXvsp60fpK +WO+rh792qTQ/a2hvV+cbe9bLll78LHf1s9nysV5UUzD7evbpbgbeqdzbt6/bPWeICGoFmD5/u4Xl +6sn2Hpuwu6b1fmlX8HhCsaXFxNbM+g2ZIVSyFLdflMS1kb49KOy0259X6bXjm208Q7lMra3B5C0t +J4xm/tk7lGLH+XFn40QmFhUlxdcTm72FglVXDPkgswYfu3KNlLOlvTV31ovdab140O31D6VnJrlx +ZxN/255C3xWBjv0ma7p3nHoX+WphYa61aH5qF/f2m7yxZFXb7WW5era57jSEDIczGEe7+suBgtG2 +sfJaQCv1MGntf2VLZO2TCc1d/v4c6qfPxnHBfD/IycrhW6VgVn7uMPH0mYNdo65t3rhla92jzXtj +rziz6WmDFIdWVE+t3e94Fao630AUJiW/r900+1vrfa4Mvy13d1vTb/l8Yj194hmR9f1O892sdWoa +opYrD2fq4sz+ntur9era3A+s5ItFBK/jxObB/QGr9OMR6FgK7saPlna7L5Uf/fL1wFqtpadtUsD7 +/J6mF4+bHzvXxf1Swc6d30rxteOEyhWB5iDDtpaLQuFlceqTvi2/ynYZxEdMKDeW77Z39ezzVEeX +t3pZ9zn7bfVhVj9b3n4gLUSEWYpj0szfX6kfbyzP1G71YnFumdt9FPmtZ883zh6ceflJ2Ne3e2xD +WZqz3vVUlaP8zl2YfXrf/rQO2kuv/W8vP2k3ycOs9V5sa9bB4u3lbnz3IC0XHndS6G4T9q6PXkEv +P7zDAq/8wiOndwCfbe4u9GVNv5jGXxQW8tqivbL8nraM47kFh0itbsy/3HxmL54qeQDxziZ8FEz4 +MNHiKujow2B/ZtEjZgF9nKMbW+i3S6CWwpPo28Kp8+Ql+rhgf+Ii8MPcjUvnxrZQvXnmfBi4FtIA +XSiM/IkfN53qTfZiwfApe8upWRer13Et3oZuOYVZ7E9y12TlkIaYTp/PhBHbtIS+FEzPiBV2nHYb +rD34O1LsjjNY5+JEuKO447TGecS8ILU4JZKmbIgvbgsl7qC7m+i7TfRI9sS5oQuDwSOC1CLOWyTY +DAUaby2BM7jl9j+wyT4YwsNGkOze4ibUnaEdsZwLp0mXAkD4ifAbMRcn4dgh3zkY64NSIJBILS6U +juTo68D9k3TI6B9AUrPh6Qv+DU8jmUt3xM7ECsRecQvJ7YHzYZ5xtbj3/cASOL9nzmu7TtdM1iu6 +zNwR8yw0Muiogk1nvRRclDh3STl+xIwbaLcWt2XZHaE9wePpvubi5cKpgEMOh2SHkIYDrVV02q07 +Ay2SaO4GbpfppTAnhHHa/uz0yC7dPdo/3Wl34xf6ZS8+LW5vTZA7Ogva/Y/9g1i2g9XPZlyTD+xf +E21LS1bZNH9AQDC+gYepx2sZThhS8iAMHRraPLCVi+ccC1U86CX4DZp/7gpY24y/RIdYO7TvE+Zu +857bkR1ZDTUqgQUWJLguA4txvyF0SJ7ZuZ5fMKxK/eipYGXuVU8t2fX7owJw45md1Q== + + + K2tvoTGjH95dNoT7pUetfb53ubOxkp22DhIzGUE+BVEYsYM8ywYiLD8sxtuHVV2yQbIQ+sqxRgkQ +n4q/+tn+4qP1BiKzTxFEWHf5eyz7ZTqNC4Pwwd1W8jyxfV36leKM886VJsJ5o6IugVM6Nbv2fSX+ +ob5ubFuAMczyIP5/43WjYI0vSjj4xF3DyIF5OVX2Hq1SY/cG8eDfBVsBufVSy6uy0pyTs58zVRgT +TVtyuDmvHOQWpp8eaUZhw35MOlxWEouCUjxQGIwqCqLCstmrp+qh9X7euVytnT0YdNGgPqfUaZCS +wqXbSLLtzVeKIuIq/k0VJoI4I8U3Eq3118LC+2FSlzeK9+ri7PMGY0/5cbI+Vha3CUDOU79N/fDm +dhaWT+K3b9j6BSApzotAXFNAVinoVtVZ3Rd0NvB43myuuAsg/109NEGUqCQKry9becs4uvv2NBNq +oULVx0Nh8ay+jtevq5NYO1BXFjzFutKPKPus0kp1+cGu5MrP8sF3aU99XV86IxKfvFb5qctALZUD +TLj8VhZbDMXszvrh3TRIYJ9TzuyvIRXMqX65W/kCjG221Z2rqTxZHsuzqzD7mws9tQrC4Nw7ueXA +HUv0yvKCmReFVK/Evi5vr78a9ca1Kb/nzX2q6OjdwRppGXtGumfIUlxZvuo4q/YpCXTz4VnPrx+u +ODeKTIzees1mi0tV+WBveh3GO5lYf1v7zEaoGY1Yf92j10wB6VEIAYVR779nzM/HqbXE1u7Li1B2 +8tD4Pk7MJrbO3tPiHDwb3+rmlHvD0dKoVnXq/MSoVw3FrE09zANBNc5hVcqzn7pd/v6dw7OR/2rn +C4Xnd3u+oJ/vA4HfPSdSmaxq+hylzrcHBshsb/nV44tHHZWdcvWS/XjRFSnOnmTrVyiMV0eErN69 +FWcH9F29WDtK12++Xmi+R1Fg8A3p00gou+Zh3VMe0OSzvfo1v9BWjZr5+WDCOteOX/uL7Z65qzex +dfe0RB/Z+gUW4/hjaud3u1xlkzjf03NvrQ8pnlT2rpcREopIq3BnvSenMmRENw/aHXn/YXfD2aLs +tYPSW5JQ0C3tRIaqiom9zce0yrEd6t7jvF40Ty+t5EVlZWft5LsGSHZ2LBd5RPs7t3dxi0T4B3u5 +edzT86v1JbcwqrJEmoaF00YR7devsEu9XUHZZdXduRmSFaMJk3OZerUOD5vr5uu3kQZ2Qju19suJ +c/iuqFBegFa/bH58FlaAPVuYzl2ut+7tt1X5Az7ua2snH+s1+6049ysyMhpZlQ+54tzsZeFlZu6i +8HKmd5Fm/c2/8R/xb5i//DTS1e5BiXbGqGfPExxzQzYjbWNhp72S7+nnS9Y7cH0ra9WWWy1R9yq5 +qZPC4t1ZF7gnpeLcOEpsHp5UrMpPftmtGfq3sADbxNwNLOGNtHBj/nP1s/r8yngYbn/FBGdvdwpW +ZeV559dQ1oEgpWbslamk5u2a8NzW2rt+eOj3CPCW7KF97Xd1Xe17qH4ZL7x8w1o8ezh4tMvbcxnr +cH/qMn8W/yrsdA6OvvBzlML0Y8isTc/CvNBluGZDK4zWsgcdzKSy+rlz/X5loElu8dwhLSyx+5Rd +XNUze0/7IoeqUR1sLlczSvZzQb3Sz9dv4hwTTKcxv2AddM4asLq15G589/FFb+xelQobu8qCpzAK +uWRlJ//5qNdhCVvH+kVxB3hLnvOmLVsF5vVoSc8+rps7aze/texNKlXVi3qzD3Jq5vvXyCxq93pj +b+m3sFEodziobG1qKUcDjx5nDOYR+ngUcLK97w8QbWYa2lD92NGas7fGebyprTSWiw4ztZnSi9fb +3zsbW60ToGNXq4cb9tt8JvCha9gIFjtoN9QdcoSGcm+mYOpPH/Cx8lKwzo7V/gI6yzvNleIe1HK+ +tP7pXRZOX709dUwYfDkP1iPwFJkTGOitT3eagCYfFs3PTPcM8ZavrSXjq/tT48u+2UzDHnHRsRNL +2jvPnMPHSuvFeN65nu12heX62sufHdw/cd1FJDgVL/8Akt2hdrT/R5jg6pfdh5r1PrdXz2Xa6zfY +eLRefT779MGLhjawfSA4S3MF08gvIUbtBLg6vW2XXh8W+FpktbzTsz5mHu9A/Fgo22VtcUOXtw5+ +PIBdr16pZevg+OoWaOneCmD6YVdcPmtYaUwYrPLtyTvW9O52gbe8fbUONlOq/Xz69mS9FxtJt1hk +uNnGwiVsBGsH1FQHokI/jXzN66fNcit/knk5gClpXACraReRlSd/8SGuxS/CBsFvnw6LhYqY+dZT +3SVLv2gcW/bb+8tmfy3wSHohfwo7iXxul292cniFCSMmd5bvtfuLpRIwMvO3nj0C90qbLhzPoTkw +rYOpd9m3lsx979RTgEuToYitq7StZ7bfjwoLh7t5TnIKWamRYM/sL9RUcF81a5vbOWyf4e1rK1Pf +ztQuARPRWbH295H8smrUrerG87R+dn5/AlyRccxveXn9B1iDG+CUqChBTaf3+uVb650YsNTN6pXw +Dqes2FYL68vTjlIj77CNZFiy1/WjIlL2z9ovLWRHxqPDbwqEazirIfvEIxKZFeCjDovQ3etNz77A +j8ObFgfwvS4W5h7mL0Be2tWs/Z96Cr/Bz77wTq6++3TfnF87eX77hZ39aL5vLeLWgPSm55avfpDN +4tvHGKWxednS7Dlrv564LOi9j7bncYdTZDOtnQP1Oi4sYtYgsX2TeM5evM+X1aV270Rd1L62VHtj +r6C+Tq3oauFJv1QLxirQMfU1lTfV0uHyKblPbhXXL1TrRzawlKQWDjPn+E/Vqizp5Dlr72dLTcjX +635tWK/my9fUHAwrFXGwCB0u5jEN2T+9+QD+8OMFP6ltFh43sVUbmTSRio2ZNIUljGpJzdnlaQ0a +Wp9qF+zcxR6GBb9esr97F0fYhrv+/jM3BxV8aX67uMO8HVvveTsJLGJyMXTHvZHiduJ0PgHz+7wS +9uQWEgEOZ6HmQjK05pudjXphcQBv+eCSQi83g/Z488TOv+rNz9OlxFbnPu/uZ7hDmwvni3fG+dnO +yWptZm3DVZOQtZ8qvNRXNeCjtfpO9vbD1nO7nS5yoFgpLBysXJvK1U/HtWHyNfvJE5StJEIF3tQo +d0Eli/PSHpCCprz+3qu2qe+Es0M2l6xVbiMU9mZN+1Xyl7AWLwuFnY+dOvDqxQodFu30GuuUlnpI +G3SxtvIxleKYZaTII8Q1Ln8/INFtAbbO1ozWPexuQu+Tq9zDtedlQKXVWcquPScuMYfuDjnjlFKL +jRRQ9HRJT8/PN/PKzeEqP2z7pdudzuLttHV48PRLyKOwd6WBkXlvwcbzuKqtvZ3Oed6V4qFvw853 +tLdXeOk+rRR25qtF/aLe/OVZPyb0MleQs/ts8aXX0C9nzE3rI6NNbyz3HkEW07Z+l2wva+hsGc5u +wfYXurV0qHR3NpfY3MlOW9ghBbXVzF5Wmhu7rdnnFVedK8XXKpVycU0tLZ0Z9RWluHt7f9uAaeoa +rg6APAJ0s1d4fep9IheeeUzlKJvnmX25W9JzK8lp2H1WbgAHV8DD6I23j45rW+dLfFZ/sEGYc9wR +ir3WLzPPPZjkuzgwb+c98e6NXrzKFvHYAdeHRi//vfN1BD3/uREU32iUX2+UnfWW3NHljZMWLwqj +wTLmN4rWUm21vLF8d2jAHrCPZmOuRhauwyxyml7PyLPJcYTwj4ZR1k8P90pYYkcjoQpNOp+2y3fV +h/XSTfPLWrW/4gU736zZz7efVyC/FOdW7nwef9WuTh++CO6KR4XHwLILi931JmIRM8bx4mIt29g/ +T3jcyfAuRjaF+d14YfbeXrnLfRjpm9QKN/vOktN+D0vbm49WFQSkzoYi1ueUsrS5/rb2vWk91e5m +OE6JKwfzBbBj/wQVkTo3P3cqwMPUzRu9qb8v8HN1/jgDfISR08/ul5tu70VZDNCB/C7m7XJvugbS +3VMGS/QuoRRqvs8BJd5DvhErTaY1uc/v3Jwn5jDr7sqfhCavAWf+taw3tqfOCq8LTypaDI+Ye+Sa +R8t+eYaddO9CW7u9eoO+VAvA4Ty97sYBaqiodaDi8gfSsS5CBY8rPAcra2szT5H81TL5l4R9nMjD +uksswxoqduzy1h4UdjXXms1f2Q+z+Y+bNrRrL4043WfifQUt/L+3WESCosYOmm+x0xbysuzELBwy +E83rGLtirroBT6tfzbdk57vWequXGt9iVIr3sXb1r2q7U0Xltb2hNN5nW6WParvU+KiGF1lu1lHY +AhdtgYNmkCM09HC/8d6MoS5R59yzdrVSfa81at1mTK8036qxM6uQhHKaZCS8XtH8ILkOujk1neLd +gPmnLp1IhBGGEpUGDXq57JYalVK70udrKz4cKR7Db+BYPcV2qdVicQFhDaI+7fuNcr2HXNbPmvVa +mfq3L8CgXTVqyOHWby5ZESQQxGw2KjXUuP1KtdGtvdcYEMJqhwXTrTVw9/iKA3o2IIwo+ktCoFKE +94JDosI6R8YFbsN40GFGSBthhLlhEgKD/KpWyKySoC+93G6+lbpHpb9hdXoidv3eM+rVagVFQ0Z9 +1sDxk6FjwRVc48jIgIIv3NgQNz7M72k3lgOHcgweG+qfX+RCjaI2qthsDe6q48POrfHAh00Uw2M1 +/9Eg6Wq8Szwd+vJxs9F0393/AbqqvzX/ovhQ1Uz0ivtezmiDXi4Cnl2aGPb4brv0NxcVc1hrVAa/ +hOvwfSu8bagysWlhQ3hR/Tgutb/Z2kiiMQsGQbFd+0GP33CRS1rYC6fv7wic7eaP3u7+o9n+5mE9 +ROfPeyU3/ik9ABLlT6iv6jt0YXW6r7qwcN8cCGKj1A6nic7s+BU/6J2gsQgfRPIODH0Eign4RzxF +FNoKa7vaRjgoOoHD4YtN7LV3raVC6ZvftHjJxHAY5Gl2WM0M7Z1RKnE3keHec0lsKOkTRzQorihs +a9yvA8PTbZe6zTaKvYa91yy1SAR2reqEfF24wV6xHxT+9TRwx+Y3GbhPgsCibdsF4Pduqm8okDIi +jfBbS+qANeEJpwxdeoj9ASa8+Nn7eWuUavVOX7iXl3GNGhYcyDZddap4iQERd4KVF857wAJVYzUU +/dgtofmoxTqwMmK/vWq9Xo1VarEKycEK3wCL3Yx14IFS/a9SrNeIIdYrxk044uWqToA0LzDErjoo +VPrXpzpabqvawI9AjT/NCvC5ZeAhYo1e868SKgxeqpP6ag2+ymX2fgXWPTwFg1Gv/etfpXa9iZ7s +NeaBYQfGsESwBqXXS3+j+G7c1H1k36x9NGJNVkx9npbQhNoaJTQEVVrx//v/xDplGJtOudbrNpOR +Wcxyu9Zyp8hJaovawnVkwAJAUlu1LHKq2QDaGDHWP4zHphtlA0mf40gvfVz6UGXQZMV/D/2iR3Ry +ZF24R+iG6QxnJNHTd1WWW612kk9KI6Sdjj6JvoQclV1DG1rybYAogJ6ka5Q+lAsvsA== + + + zsshvioCzGa/0RQp4f3nUtj4lYYeIQkxBxX02cRbFk0OkE+H96EthLgHtY0v0n9GOt06bV+rFcKR +oUF5q3V/SojxEShuNq8GtfRTjMPvKxS3EWdmCHmm3k4COaRYyYcPSpft8cF9hV6i4upuOhVNCewA +N7d+wEPlUeA5gzfoOcLjhbWy9fHzTacE0YwQaKEHqw2UcjNk5srtSrJTbtVDhVH0ULP9kQwDMqqt +gzKP96VA6K8Oto2wskh1KPC7FL51o9kvYyLcRYlVQuQAUm+50QnDGzzzXq+1Ppvtf0UYDJpN12+P +QY/gdAkDW+8kWXirl8ohuk5UYpdjTAYO718DkU6g1uAZRB+Yo2l9QzoIoBUCqQ1BJ1CDbq1bj15u +PVzzIz7srOJAGLbJtjgAPGQhtQeWhiuO3Lp2OE0jlSLm4c2RW4Okf/wsEAakOhy0jaBHkX67FipK +vje6yUq91X5vNsIWC3nMJav+GzCq8geJaV5kBNXc6b11wmQzscT2AFWH+LQDigjPhu/vrU6yUf0A +7PwVxkX+s5UUeMWAkupKGQs+oQ8BBeviDPZ0Pnza30kijrVR7YTLQfBc57NUqbarIVsmrtGTKibg +OWAOxHoDnnvD2TJo0wJGovpXtR5GOdqVdsfLCvkNe7NV6YUMFS6GsfdhzcZFdUKmDz9QDllNpKpe +oxy8kqAMtj9h1f+Awgbu1m554fYH9BzsZGEaDXjkw8us+WuE4Mm298ngnREaVy+1Qrf3nyRLDtfs +fjJuZ2CJoTsaHr5So9EM29/dess/f38HKy3gQbQ31hohIwzPuARyQIoez2A2QnUyjnzRdNm7BQub +GKmIXopV2kCd2gE1IM7UrSLYHDq4GRUs+3Pb6ILdg/2yGtu/PI2ZzVIXpL/C6e6FrmaDBDo05M1w +PtXpcKlDRAhHGuUsrzp7nrO9OimILq93b0seCwl8h2wzRT5jrPN0sfrTcm283Cs43RZRIYnwQPca +IFU7DJjs3rCKllOQUwVK0HcI7BW5k3afxqfzFPuzi8Kts493304cNcsiL+PWghRISFbutEpetQV9 +mdipunzDuXH4Z/e0cVZirEXKffWsXS3XhFxiXpnfPaVJPOgHzRo5AAgVz+5JWLfHf+ObXCmxlbG+ +09unr1tyZe5yB/+5ld+b/XJvpIxcKqfdaz/3ONkI9rDgXtt7qZrt9lapsVu//nozXo/PdX25oTyt +byvXOWum0rOluGXuPT4pS3q2kZkyF8/TndQsijF6W5VXE8dqeuN8bT21Xewa1nt+93tv5mKzZL3L +d1vOXTWxeaF9Ts23ir9Ty59fe1OJcjI+tfxSeZhaVguXUwt7HegL+uIumTrKTyW2NltxWstnN7XV +WtxoQZMPfmiTSxcG/e31YBv3JbnayfzCb5etvkegf51coVFUF/J3s1IcxknB3Th1W9Z+7HzkoOZc +L7G1OzWX7qidY1Zsbjd1sz7zDn/u1uHdO4t1/KjTbq937ttP62en8mr6cgG3FVcKtZBqd5XH9O3n +0bJvpU8fxl5gpZr6tToVVOlb+3k1cQO1iNXSSk+MxZmrVv3Ir9LOzLNmBFW6t3WebVx7KkW14GrT +i7eJ9fLpsV+l7d7r2sJSfGPq1a9SuSBvbwRUqs3M5tbf8xjJPn1N3z3KhaJx7tvT6UJrLX5aO77w +rXR3pnnkqZSuF1zt/MFR+jhogK/aj2X1AFW62De8u9M3qXimOw+vpZt9c7o2C7NPqz2bn/fMarqY +3a3jSmE1vdlipU/tp+u3i4BK114yV5WPVbdSKc5V+7xSPAmsNLv6fjnvX+nG1GK7s5bo+Fd6ln2G +Wih++/ramd+6UwIqzXwuzhvVXf9K04tPiY31H66nOEmcO6ulzMyv1jv2q1QuHJ4bAZVqM/FMLrMd +UOndC3IlbBaLvn2d3p3dnDuqfl75Vrp7nrsOGt69uZWp5Cep1H78LgDG+AGeXuzM7yzjAV7qq3Tv +5Sfzu9SSodJsy1vp0f7JE630bmXB01Mpns0kk/dutUJf7w356Oc861/p/nQvd3RXzvlWelqv7rmV +wryI1R4mf1vLAZU+JOTLo9+ef6WHqeejQmF7yq9SmJfifm0/sK+Xx1rxNahSS76Wn/P+lR6tzBQr +L0vruFIp7u3r9fNmJ7DS64XqSyuo0mP5ZmXb8KtUikO1duJmJ9c2fQf4frn4HFjp13TxyAio9FGT +n0qvCVwpwpinrydXte+1+Mmyb6XPz7cvgZU2q9sLH36VSnFU7bH8ah9Z/gNcuFKmb7vn+36Vttsn +KzO00rfUomfRJPJLdgpXKsWV0nR3V6RKa+1eypRRpct9lZ5uLvw+27c7UOlm21NpYq15t0wr/c4v +uZUCTUbVxt8fpslWrhp3yr5IIC5k++RhF1W60r+nnqzMNlLrZ1Cp2fUOr21/JXGlaF4W7GUPKZyv +JChVSs2umYciKawmNnMnD6jS1f5Ktfj05t3eHlR6MOVWCrVgdko1Tj9JX7fXzpOeAf5qbtrfpNLt +q6MjcXhhYr8+m3hPBRbqzOTvqt2fKXWt8Ua5i/77vbmpVOWn5X83vQiLZitTDbrbBjAc19y7IrVM +32/KRzspFd/vJ+H32/LRxXY66K4uH1UONb+7GMn3pnw8dZMLetuWT8v7F0F3X+RL87sbcPdhWb68 +npmiI+ZzPykXd44Xgu5m5Kup36T/3Y0pud2bytK7/ftL+mFPvr6a2iT3vQsp/XAgX3+sbAfdPZJv +ZvO63108Yg8n8o1mm0Fvn8n3M9mnoLtf8tPt/VLA3ces/PT1vsxGrP9+Xn6+Wk8F3d2RX7SXfMDd +l7aSXDrMuHc9I1ZaUPKv6nHA228zijH3XAi6e6kcnk8fBI5YuamcfKu1gLcri8rd1+Gc/93Mc6O4 +vtG997+rds6m5hcPTuiIqfNbC3vifX0qubezRe56aZvarU9tpA++uLvmytIFL2EtVIoJo9k5YRSG +SGDp5QtEdwxAXtXwEy6p+LitxHdWu/NmobidvbPuC3dF697eSMJ3csE0kmXTNFYPE7x89hqvk76k +CCF16k5szaTmsbCH6RiSZB5c2rZ6nGvMy6ubtz20Nh6A/L1vOJLozGpt820R1tC03cmdr515eMv2 +tDq/ebZCNgokyXB0nK8084kkmaZ/pem7W79KYRfD1U4XKiId5yvFkkxApcC8giRTCqr0FVfqIlno +6/TuSp6rtDI3N+NWivl7p9KUZ3gRd7/OKt2t40rRvJABnl7g+5q+nHUrBYlPSQZWivn7gEq1GcTd +P7q8Ja3W6etDYKUwvD9qYKWYu/dUiiU+Wi3i7ytBlVaDK82dnN8GV4p4Bo639A4w4hpegio974PS +zNoyrR7/RnG+5jP7/k+uRyox/XgU+JwUF55cPCNPEnqhHmmi3kdYuIUeyNLzD6argsE8GiEubGRT +jMLAizvK8pWy7Hw88OI4jDca1Ba/ml4TDVTEhdOGcxDI5pqocaar/3KrR6noMnHygSb0hmdzaQWn +TjcsJN0V8CMe7dJWfvfqDP6ci9OP0i3HEZPZP3eXLjz+YCn2U2eXI3Bck7fsOP1YPm6S0SF8OSPH +bg8AgSYZQG723ZE3Vj+rVhx9ACS35X23UX5Nch4JaNKyjTWK8B8bUcVP4YcHvRc46O6Q4w/aPyw3 +s/45NJn08GyKh4h///DHxYD5Az742J0/pIXzm8GX3hJrPJFa/PrXdCZ5wPxJ8YEzuJ4YcrD6i0J9 +oYUtRygsEth371qecWc6paGRJb8r03eDVg4bd6yDDRn5QTCNtnLwjrwujzFYIulJNvtIz91yC9YL +X4HT/yFnw0bd2OUK4JT5MIASozuvi3MYl/5jd7fcDW8Npuz4g44dVoz6rkr78XrKj3AHr0oiuvh0 +bVs5PBJWpdi5aF1bmYkw0HL1LDmHrQVUQ9I3yrtnP4N7NUd65Q/2Z1uudr5vqO7Cb7YizlVL3CFS +/jjfPZwjGLvwJS72o7U61MAEDMtLm9Gx/oHZkavdq1WKHQfJRELxLeytE1gUzP7AwsR1p7T61t2b ++hveZynquisgNdF+4LpDNGseeL3jZYcmLbiw4LWj8HipoD71jMNBE4o+aOOJorkfG9A/lwYSmiwU +tmws0EbxLUsEALZUkN/N5ENAJ0/PMfuFUzfyXFjYlKR/+6bkO9cOp7R4xilv6Ud83MehV7edIIYg +Asd4yu1iu1666t2YwuZXGMXKrlKand8TOFgf/ikS9/Sdnxo0WJtvC7hJvI3Pr1GqcZ8/8N8rndYM +bhLi+r7zM4GNcveNaPP3Pes3f4Tnx2R2JnwGufkTWboI88dRfu9gydXSy9VkwIDo2Ff1ejKFuRuK +pyigycMX9j7/ezuxERvA1w03Yu9rC/cTGjEPRRt2xKhei+lhulu/HsFVNW6fA4kQ8JZDcMcfe9FF +QWGD5ujYHrKh7EVksH05BejQ9/SEVuUesnIcDiMoLwZi7GMvNbt+eDT86PCtYTTC4WBHGJ2XZFRR +IaQvg4hCpIbIrpQ0UGoJbMgAKsAaEsJboqaokcWn4IbAoqfeHRFkQ6hq7dd/1+xudUJ2O0CJ1C8b +YsOj6EbE3lnBTXLV2dCar32gAiU7HIuSUIGg1uDG7qnTiKDUcInHomcAqacKbpSH8RgkDwc3qTsz +QN6PSgD2hyIAhIfx9tDtX3QCENY/Ka4+dRPz4ww6h4MgTsHZX7yoDeDltxesrrJkfBWG6p8UD5jB +z+gLMkwmB4ln9/FXlPjGGKzQfd3RXUQbrPAlHggGSpPFJZ7sX+I/B+ISDxDSArRLvIYkNbs2PT2m +LuHnQNR1pVyPu2GFim3lcC6KIsDRMiIKE7CUoGvK/NhdS21fF485G1+wgiNwC0rN5n/lCL2SwhUc +B1DLdXPsDiEOVtxVh9f7bCsHPZGee4dFijowmeE0j6K+hqP8B6EaGz9SIGgaRFZ6e+2s67LSVGud +2r6KR1AmDmalW4d+OjNxvQweu+0rJVDXFbwN+moUD70boT8ieE44sGt4GxxzvWxfbS5GwLkUD0N6 +69C7/Y2A87XzKaSB53e+0ToUT0fS9YUSgEO8341DAPC+DwPj2ep8B2YwL4tGR9zoApEciZedh5pn +5jlTLPxmwncLvspuTuKLuO5uL8PtE36IcHgYd853z3789rtRVGioKKRtHbxeIuhboTCRBx2JJqNF +o4yjiyfzguZtcWFcnT4uZcmvFEqThyknMXxrBG9bVs7yRHq14pYiypXRdj6xsGQk7iKapWJxsZ/f +vC1GtTCA/DJI7wWFjbvduHIlatnui69zRrCNL3AoK3PqaqR54c3cYRQNRJfMooeiwXdrHBvn9R8b +iqJdRaVoUjzEigds5WQoGhoxVNiELEhQlB9FG5JTQnrp1ATWPjLE+9Kioda+x/chDGMDy1mJ2hok +I4eUE4V8DG7NqseLQDC9fDYTmwxF/ryAOGGJQAkaa6440+GqL5dCHb3oUkESz5LXqQ== + + + C74LJuXD+VyZcGM/AhHycmYe2+vt9TiGXG9RDpENt+9H4OpRYYL3g19R0kBngd27Fpamx2GlifS6 +LLo1jVAOLiWQsZfiw5UT0QXCUwqvhyHlhIrUUVvjcTzysfCGseTewiIqsqRIu2Fpejfh3Q1L00cR +ZJoZzo4cuBveDM/f94MUaRQnxt8//gbuhSLGouyGj7/h/L0fQMQYKzyrynQUncsAel+aPp7ILgbl +RN59wnYxKGdsDyJcihzJF25wOUr4XijFI++GMGErYQ4SIXshk169u+HawnLfbri2EGUi/PbCPk4J +mvLQjeSH5bphBPZ+bUHxWBL5lgmMhbsgg6xvICqEr21pmNUNhY3G6fppFV7akVZ3BLysLWghIzaU +cAkzeT2Ay5QG209vb0NV11E88zhNL25UsIoiAqvp2ZZU4/ZuBW9LQi1e34mRhbQ7z7bk8R2lvmuh +nmunQrs+A9XQ3FCKXmqBYDGTA2Sj6N62qLBIW0uQQ71YlDYpDYlq3E3PRZhJbt8P8kJEcxnJ+ZbA +IlBGfut4vaWCEBGtSX1qYQ7JEZZXi18W8Ju8WnjxhTOdAZJs5fRTiu/Wr0svOL9KIT+781wobp3Z +w8fQhUfQ0ajnsWPoWOP9I+jYiI0bQxceQUejnseOofOvlEXQuRLfeDF04RF0NFpw7Bi68Ag6R34Z +M4YuERpB50QLjhlDFzi8OIIuOFpwuBi68OdQDO8kYujCI+h8I7lGiKEL94fm7JVjxdB5HJI9+7VH +D/NgRXJ24fi64DigX88uFr1R3iYN8J8CXof59Q2ImBJVTKP5r2JL4oMl8r+DxylItj2b9/Ul5608 +UcdJVDEFj5M7Sr42catvo+9zLZoRRK6wIDzkrCPo+UfHUzMcmpJvDwP61xc5F7V/ok4JNSoiOAc2 +iTflED3/qIM+SHMVul6iBM0NYpsFuAZ62z7bI2oP3fqg6addMbo2mjOIzxzYEWxzUkS18bM9vL7K +7RqLFbUfH2fG8XmgwW4BriCiJ+TgYLfRXEE8MYnW2AYXewDPH31gQlxB/MWQECRHiNAKkWkEFyy0 +Kt/UpoeJGKwsjkYe3tTeIA1J1DghaNLLAAdKKWqQaSHUlj1Acdan6UVaSDWamiRAcSaqDRGnu0y8 +O1z9/S4aGHtsdoKEpjmLiyGZV3NGjwNDcXzdQZzgEHF8z73wbXKYOL5BvNcSr7QKblLVGwTDy/sR +ggu5JoX4wJ9eeAI0B8XxDfKBHyaOL9iYPNT67Ndg+vKW0QsbEGDjKQpre4ILW51MJ5HmavN6QJaA +YTrpa70YdcQGePsPN2KpcTrpKn4Jb0mSQAquTh97g6JnIvGye1jnGxox5C72oIiwAdFyrIBgu9je +YIKDV7cykDp/7KHIlMkIe93t4LXNWRMWAlWoqIi5qKJ+EIXpbnXkAclRnJkOGpPwQDkpHmmSPZaR +EIknwCMChbgFmxujjedWJ1yEkyINx8DQ2KVwAxalMKhD6agdGuTKv2C1PVwkQnLnozuE7BsY6WMH +h8D3ITk8/Gg4CdphIL221/3JqHdwk9z17sOPDbPiYZyiLFfO/hISZjeUeseH0SYc7P6w6p0hI+M8 +0U+DGsWaNJRGJt10cnb5NGo0PPU3yZtTZfRxGhz3GtIoUSOz2fVqZFBIUgSNDIexIBXiz8HYGhkp +nppdW5gNl1QjcugHgRoZj+4igkbm52ACHkTQtfTC2GFoHo3MSPmUcBjaMBoZ/4hUFIY2goOwd5oc +jUywFi7SwEQMznH9lILCc2B0/MJzhgrO4TS9q/3McutwUBxqJGb5EK39cT33ttfOB3huSZGlltT2 +VTZC6KivSse7Ix9G8J0d3LVFT9cEv4toCD0cPm+YX05IFFcX7LoQPa7OJbI+uaGix9VF9mUMlpIO +I4aQBISOCmOD/JODlaADg+s8jnpAI+e9dlT4bnGQj2I0W9mgeLiI+frGjIcTbUksIm7S8XAjY2yo +eLgQD9UJxsNNwEM1QjxclIjU8ePhuGjBKNEsI8bD+VPLScfDuVFpw4VqDBcPF56HZFLxcGxexIi4 +4PkdLR6O1uKJiBvZrLN73ZxAZD3a3gb02Y+P8OctUWGRyEcEn0goSp2Ed/qSfhnJYhXKqKLQwzHT +kGL5BZczdloLXIooPAfFiQ8uZ5QYe28k11V0U95AiuZmAPbPpjW8xzMw4ltL3kAc+9FsDfIiiLYM +76IkhZUixDINyJoSgZ1nNgsoLNgWOqRr8nyftDgCN24O79jgx42jkMHxswHjUugiHEd6xeX4L8Oh +cnfgcsZLdYFLwRibDGuPm/Tmm0wxOBNFiF4a9a/Pqwh9FyRQ83nhokSklqYvIoQvDKJjN5OLSL2Z +ZETqzWQiUh9/JxKRqkyvTCAiFUqZSEQqKmcSEamonPEjUlH0migeihLf4FhuzwIJSprqcTIKiubw +LsOXdv8yfGlH1WsN8LadUCics4uxYLh/SyjcGLmghwiFkwYL7hMIheNGTJuMXOkXChcmVwYQrhFC +4ZyzBf0bNaFQOCIlccFwXC0DQ+EiMoZvHehLMGkaLiM8CnwSHSREy8jwcXXf4WKR12KDuXFfmw0q +7Dfc/BPVJ9DE9v1ueKq3qDzMnTcReYAtKUIeWxzCFiG/mpAm2Cca3UyuhphwhtscoEko07fgdTOS +D6rLsBs4s4dPfY7HHT7EcuFiNovOBL+cAn7paGolt/IytXS4rkwtWxc3U8vXz5foWPDi1FJR19Bv +Z+g5c2rl8CUjr95+Z+lmtNn85hu8N/tFdUpisNtCSLDb2arMj7EQ7NaZn23yB5yKEXaZz4W32Yvv +gLizxcewYLfnlcBK5YKhnQqekGI0lnC0mDfY7Tks2G0q41cpi7Db3W3fOX31RmOFxJ1tTD2FBICd +71wInJIn2G29fnUSUGnmc2nnZqkVFAB2FxJhBwP8wc+qN9jtaS84wi7+vXr9FlRpKTTCbldeC6y0 +3fk4mAmsdOpxRrsKPI9vajnsaLz9Oc+souW6gqvHv7FIvF7F7zlMk8UnT2eqUUqcPt2civBcu/fy +HRfy9KJe97GiTFkMby8veLbTMJGKd7kNyqXm4WDP5n68bvJefWqYz8aAc6zEMJdRDv2iUhI0auc3 +aqNCmxR8Rouvz9VYJ8n5cb8+2RvGPElOmDp6jlzwSXkRx2l+ejifq5Bz0QaeOCL49YWe+zb+IXJO +Ud4j5Pq48ag4GHzYCNc/4qsQ2KiB7uHRmhTlnJEBg+40KdgvfMj1Ir8fpR8jNYnXebZErm8S0XSB +1oSJRtP5cdtEbznJaDo/hZefpne8aDq/WLrgzJOjRtP5OYAE+CiOEU0ndIjG0g3KdTN8NN3wWutR +oulCkDzBaDq/WDqsVZhoNJ3fDDgUZmLRdH5ys28E91jRdP3tOg6zvY4YTecXSzcgZmSEaLo+5oY7 +IXdy0XR+s9sn748dTccPFuOig85LGj2azkUJr0+edDSd3/y5PiSTiqbzi6Xz4ZTGjKbzK8rJCTmx +aLoQC+8Eo+n8YunGGrGBwTnDjNhw0XQDRmxC0XR+sXR4F5toNJ3fuuJOyptQNF2wl9oko+n8Yul8 +/JTGjKbzi/3ykV7HjKbzi6XzsyWNF03nN0NeX+vxo+n8YulC5MrA4YgcfhNkeZ9ANJ1fLF2UHERB +5gPUpGgCoNcPVvSoXOv0Be8sWANZjP4wOj8p6Wt/0DGVUQOfGLWIyF0McV6dH8MThbsY7rw6P/cJ +/jy+gdxFtHHyOdnW6wcbcZw+B27fAgQC85Cgc+8CTqIduklenj8SLn2bFHoErRgpPKhRQ4XGLgZS +GNSoYFeK4cbJm6sz2g4iSkSp2fz3qigRHXizi/TbK6OpwUY85s4zYv4H3Y3AkovH3EU8Z2TMY+7C +dRf0oLtxlDH4mLvx/ZOjHHMXwT8ZBdKNecwdojADD7qLODDBYUoBJ00EuCmMesyds1eGHXTnBFWF +H3MXMS8c7Awz44LhcIJxFq3DoSKLsEYxKJp57XzsWJ5DvHUG+ipED6SL4Hk4yNcazfnYEbA4ijOa +92/YwkXH04l75ShRafhsugHEOsyJTDgjFUYnO4YTmSeeaMEVlLlzrC4Dt7qhNjqkmvcGdo3inQ4N +TQ/QDUf3hsI8/6S8oXD0/7ie4ZeBjlBDRj6Oc8akI+/jcsaNqcWl+Gx5IrWMWk7YCox+TuKEDpok +RQGvOphaRg2s/aou9AXWflUH2pKiav1QYQOCV6QhAr++qkuRiBmnLvKcAMIP5mUjUowyL0iGelBf +N4klUfShvm5OJDeA6fqNjBWZcjXJEwyvJnmC4dX4CQLwSXk+/PsIkY+J4QMZ+r06UTljG3dJKeNn +CSDlRGTiifwSXE6Iy9AQvqr0nMSooQzRAhnuWp5FSPmxCS3DoBPuhouvHPWEu4DYt0mdW01PuBuT +G494wl2UyMfxT7ijJ+WNvwxDT7gb9qS80WTpvpPyBnnuDYwP4YpyUuSEZjmLGFg7+IQ7X6+biPET +j79R3ZvC6BgwdwNC0qOyOST2LZLkGCWwVpn2PSV9yLOeYbyrEWIvIkQ+TiCw9sajzRrR5wqXE120 +DtSN43LGD6y9GZAXbsj49r5E1aKP4ggnevU716BQqavgSGApPsQyfGmPGMTku4vdBoYxjRDENL/5 +NiWFBwtF9bBBhX0GR6hzWc2jCO5Q2HeEVRmotxRHLFR6H0KufGljAT6yXBlAUNcWkhGCmHCW5ghh +TNCoEPl7EGOI6JiXNXzrs7ji78ZgDD0n5VV6QVM77HGPZpIjBX2WkWFjXN86ESw2zBduUIwrFDZG +aipPnqu3zqSOezST2ZARGy7G1bh9iXI2hRQe43o3fIxrSA4i1KiI5xiEbQ7u6t3KHDfc+gQfEjoH +R512W+3O0iC9g/wdiu0roo+dqUQ5eYDC+kwc25dMHeVnnamb8zSO/vbSnsZr34mdUuJtfn2K57BN +q2v5gGPupucC4+HavdfkiiiJiwfdbagfwSfOhZytl76791RKZt8J2PpSAiuVC+8nl4GVzikHL+Wg +SitSPOwcNuOSq1QMTevMfFaD4uFye1u/sz9OT5HWWoxy9A3DYwO8GXbiXF4OivzTYMRmL37U56Aw +vJCAw+lCJx1c6e7y67VbKVr7QrXz1YXsZ1CUYzKs0qP5wEqleLtztTUV2NepnaeVojCr1TyrHv9G +J2LRvK/8BD6H6Rh78rn30xhYojbz+2zfng58LvNJcQebJFn7KEjmUfewnUwjs/DTt3UWeu1gcuRs +dO5u13eigcitcvYgRkEfrOHdJoNOMTubLwSomHw1JCEH0CGeN/QUs6infA10xJTiEUxdMGsF33w8 +UTldjoexxnatcsfJ41gV5gkZPk4+rlWBBsEBUWlLgeLVsFFpA3w0h8BToJfWkL49qH8DfdL7++cn +WaAQtzA/rWGaxEXXjjfoEby0oq6X9ZBT6Ulr+n1jvRlCfNJhPtsT0THb/WemjKCDvQ== + + + W+5GUG5FOvsJGnod7gozQPzlKIw9ASvP3YqvQ81w+jF7RL2WRz+GogDHNlmjGEBRk+JDLSNFAUZK +UuoMi+9eaU8uZaNNc92MUZjoNKJ4bTso6u53sLU6CoUpFSYnI7+lpsdRBgvnJJamjyeW7AoGy6vn +HzbzkEen8p1reRwIPApG/9NLo0e3BfNtUXJEiKGAg1ziB3mtOnbk79yAPAdDhEW9TXmYN+/5laI2 +Nji6LTjPQSRuXGjUZ6Cz/nDH6mHfUYHejxHKGXjmjZsTMnIoZ9sTqBMBDEER3KiwQWJK9HahDCHh +h24PUVgg4kcZsYGxPMOM2Ig5R/xHbOAZx9ELCzzcTYgUZoX58od+UYBRYwCl+GD+MDgKMGoMID5V +duQowKgxgETPP2oU4Gj65GGjAKPGAPqfwB5QxMgn6rneUKNEAfbNUEAMIOepMkIUYNTxdPbKkaIA +o8YABsnI0aIAB4ujwda38CjA0yv/XoUdysfikf+9h/L5Izk8amv4Q9R8JYuJH8oXqoWLGDg8+FA+ +KfI4jXMoH8f1/RsP5RuohZvIoXyhMSMTO5TPJ6v5v+FQvoB84yHjVA1cvTukNYNzQ418rl+E3FAT +ONcv/FS/4XJDBZ/rN3xuqFHO9evvGn+q34h+Sn3n+oVrhYLOrxz2XL+gqLvU+LmhDqI6TA2Kr5xM +LASNSRz7XD/nDd9T/dCITeJcv0mcLzb4XL9wnYMnkmvkc/28XRPF+9HOr+w/128EveUI5/r145M/ +1S/8dIbo5/qN7KE61Ll+4af6DfCFi3yuX3jEjIPkMc/1GxQxNJlz/aJHpU0o3N3nVL8gPX+EhDfC +uX6je0EPc65f+Kl+EzqPb2nQ7E/mXL/I5/GNda6fU4rvqX59FqsRz/ULd3Pznpsw6rl+4ZJasK/1 +cOf6hYanFCOfZDTgXD8ylEGn+vVbEkc7148F7vmf6uerhwl1uPY/12+EqLQRzvULCgVTovGWEc/1 +m8Daj3CuXzgb4J7HN37cQ/CpfsOfxzdKSgG/8/jGj3vwnurntfGNeq6fr+3KsXBKUXn5Aef6BXWc +LEJ3FxvvXL8oUWnjn+vnxG/5rpxQOjbEuX4jcOMjnOvnAzTuVL+xz+OLdLhmhPP4xs7sQc/jm8C5 +fuH6Ly4yZaxz/UbKqDP0uX7hArU3w+Go5/qFn+rnI72OdK5fOJsjTehcv4FZmyZyrl/4qX7Dnsc3 +mjar/zy+cemv36l+o/hc+ZzrNyAYHmNsAuf6hXvnOGcMjXmuX6hey8QW3gmc6+eEj/lKonQXG/tc +v3CxHc/LBM71CxfbqfwyqZingFP9RpEr/c71C5YrgzTwo5zrF36qX9Rs89ECYoNO9RsUKxr1XL/w +gFjiETH+uX7hAbH+/Njw5/oFB8SiU/2iaBQjBcSGnuo3HA8TfK5fuHHBLzvQKOf69a1P4VS/Af6W +kc/1C0cEyaAbMVdKtc/OiL8L3hzovsDOSwpW7b5fra16VbvwXYgnq4/jvXC+mCdwUcBTy6PDgsk5 +M91lLyi8Eg1+CO5WFrhIYeRt2zDpiLLC6IsvVbPd3rqo7ax2Nw50JXdzqc5vTVv4ERRPtZconpXa +U/HHxNwUUgRNzT/vfU4lN7/0xPrWL4rk2li/uEsUa99N2ba/VmX7a2VNLhyeW3KhWTuSd8/z/19x +36Ed1bFt+wX9D02QkZDUqhzICoAwIoNAYIKQRDhGElbA94w33v32t+ZctbtbARtf+75zzjDqvXrv +2hVWmCtU9axZuXP/rVnZ/vDRPPiy9ck8ueereXrn86p5tv/pg1k1B1/M6purh+b57OqkeTnzbMq8 +fn/2gXnz5sUH8/axPxDN/86/uGDe3Zt8tLe3d3Nub/+X3ct7h3Znde/wbZncv5B/AiB4fICdnRdm +P28uP1i+Xz/cWH398uOZiz9NvHh4vlz+sjDx8Mntn3/69OvE2bN17v7kuS8bE3dC/enB+3+9WLpy +sf0e397huzOHM1/D/XdcEt32Nn/r6dMJc35rU2gPd0/VIW1duLt0fx/bSVfOzC6shbGfgNQddtOX +v1xa+M5kXQoyHYe/mXdX3kzt7d2fnf6DkYapF9NX/IUb5tbC3QVz68PGz+b2g/vb++fepPfcX1nP +tN8tvPbbzemr5f4rM3fr7RnsSXxkbq7mt/yZPzP3YGvmmDQdFZ8jP9n3bnJ7FG1FhGQ4uJH9GZ+J +g5/OTH3+cu3MjLu1cmZ6Y/bRmemXk0tnzi+VB9iA+3P7Sc15//DM7NyF+/jiJebuBX5X8+aZmXsv +lnsTZy7+dk5Gvf5bafytP6XppqFBz9uLi/by4uf1OYvxvb1xeeXMHpfp1k13r8inJ1/tzMeP1+TT +6m908M3cr2cHfNafr9sHxkwO5lQnn4d6nGyf/rV1UZ5ZntL3fbjw2zQup9vlpclZXM62y5Uwp4+J +Pji4ufHrb9XMxTtmfnt3ZX/+7vPnr0UfHIi8tI5ey1Ojr8ZHcO3q9NgX6+cXr3ZfLM6OvnALL59d +7764Mzf6QjDYx/nRWx7Y4VdvZPEmvpi521cvjmjjb769ODP2xdibb98ZyGzPXBSj9su0tHJuyr3e +/9chVv/2Yyuf3xt//dmZQzP38Mao7beEi0K7NYNnLgre3lqgXoFSLKJVV4oopgeysA8fzCHGPEMT +LJfPLJ/g4uAtD1/5uXtPfvXy9FOZ/nNXzuH7aXnBYNvMra2MJuZd95Z3svDp5ezS3PqVny593Lnw +7NaVm+FfY8pTVevtF4+G3uuRgH1nh1dqpzz/Yountce45UrtNCxbnPq5PKrl9q0bF7aeLt3ZbD9T +KaN6YTv+feqml2YmDm9NLt+546a+vD3X2GttPQwHvk724oJhxt4/mMUUzci05h25fDbXGP/9C2Pf +r9yZkk+vrOLfufdv3LV6x8lavV/3/HTUAi6OmzzhofHanjGp/KsK4JehArgO2ZdPX8LKMQUgsg8V +0BTA8d/SnfsJg5xsnoUo6zagB1sXKYtieJ9c5DxgX+8XnGWO3/mMhg34689vrt96O/UKFREi9gOI +86T+5ObW/q8odl6+2KTbnp1zS7OvpkWmr8506wLZF+ggtMUBoMoL9arln6mGmOOtuRGs6h3JB4lu +6JgF8Y4u6CEyGyY5KrM5OXmpk+NLHMakvfnm0pWOdoNyBXTx+De5vDUgM/QmVEGIVP668OXszi07 +83XRLP18Zdec1AfihOsiilKwggygI+9Pc/3G0RrRCrDscInh8V39OoIYFJV4PMR2XDY67KHA4uqT +m5tmc2JnaW/v6sbR34UFKLly93nbLcjb3eLT92EExUjDD/3e65p4N3G8icP3D38TM1gO9Wecf3rp +fh6z16Sdubq2zWooQof9Z0cOQwD/hrWH403E3Vsj8wczqXvjqRSFGe47BOnBFsvT+DQY0uYY51eq +sN/ME1myg8k9lWSxPy/HzG035k8XJ66ceTd2PkHDI/pbzr+IYgq35Qs7GA2cPMZd7YoQ3ozBgCtL ++086BMCjEOq5U37XuPvNWZnU/bO3NuemR7/lPDzloFV0a0Ji+DPQwxMPXh09UuLi2IBe7fn33YAu +HTla4l347Viv2289nzjb4uDab7rmh+9u+OnRDzDLZO2O/QSbCMBg1MDwDAgcx1DPtoFvPLjXo744 +e+J4iOl7g+/0my9tCzE1ef3V69MWQgZ043A0oCN47IcXAqdIdk38Mt7A5e3HwwZejBogtjzWBA+O +/BvMoBHv8T4M1+U7vTjZxNu9vzQMNnAkNt4OOfo7w/h0eFoD9F+2j/7i8i/fmbFjbPjw7XGGHY3q +yH1b+0MNo8ettDsF+F95eOTO3TPfbfEHBgmpfLh3dviq7bFXLd9b2zjiIP4y5Ky1X8a71H5t/Guz +hQfXp6HZ00g7NZ1stg5uFArfyKCMUIqgx+1faFrFOq1FNcYLL97SYs02Mxl/2cflXGe+ly8MP6Hu +AhYbdvjiUTtMszzTLhcHNNqD4YNz0zdXXyyjN7/466tfFzpbGSbHgPjQsGJdxLSO4e2j9nUMb1+4 +vnKt+6JZ2mZnD9/c6L5YGYP7o9cTJ1+aoo0YN+q3b0yP4e2xN9++NQK0b7jxVGgrcw3c3H5ggGGm +/flLAHS3nzmCZUZHGwIUxLw4TUTd8N/DO7OdXTy82GzXw5WBgu4LV1+IcD18bKAyAec3cflCm/XX +n+6oq+Smzg+mehMNR67d4VpOjzsQaw8GfItfKL5c3noz9WHxU7r1YOFLOVfGUAEXFohSmzqexh3l +XqXFx3P/TItde8/MqL3eRHp29dLi/LOfLr9b/JTvfp1/Or/zXBH6wquLb5R/pzZefO5cyqd+yF7v +xtnr/crMGBBXr+T94wGRZ28CO9inFI2vn11VXD5CpuLJ+WWgqzVhpSt3ul+dv7czwtvEGc3VnS8X +GthQjn+429WQCDqe7gTtzb66uJ/PPvxp6OLud17pQCPGzT+LYYoFPtLNG7i8NN0hQRGzoTTJE6uG +si/Shpg3KrAHxK329pvBSxW+0+Oky4Yx0WHm8qfui7uTlGM7Y3bedXN8l+ddkLo48X5InWm0ZxYS +f3cwpmtmPl6d6/DtijHx/V2klu7asVtuL/yWhFU+PgC+FV2ytjsgUjo3pacDjcIfu/99rVesy/3i +YuzPPT78srX3YO/zx887/Zne5d7c/B1rn+1s7t7a29p6uvVfB0u7G4fbWzsH/Uv9ufkni3fulLi0 +tbG7udWfOfJbSWMxzSMROHJiepm2X16ZfXdrNz/3ZvNkfG595/aX1X+9X3h379H8/MyOfX35ul0t +S+c2D8VDWlpc/uU1Bf2UCCsl5LJI8cHC0od6+9flc4+vri99MGvXxuSH2PbC16e/wfFaxiFo4na9 +3XwFp+3Jmcnl/ZkeCWtqKRT5H01GIMoyCnseS/IRrF9LE08mbtXzZ58uvv753PJ0jV8fLry7m5/d ++PD06+Wbzxfu/dzD94tv55fTu/vzy/7To6Xr00+eyle/rd6YO7i4JF+cec8wzlHhmLTTv2YcfrX2 +pmOYezujQMRIaMVAfmkrP9NZobW9ptIOJsFabw+anhUx6T7ZScQ/JpsyjZmSi+OA73ylQ8hLkZBX +e/QFhxIp7t/QVzTjHHvh0zBMdXdq/It6sD78Ynr8i5XpjeEXs6MvEBd/k7eGX82NP/N18ePwi2PS +cFEFYYz2bHIobPdnxr/4GDZ7E8OvBlS2oiFuGNVIy7O3gIfvWxXQ5euPcflovO33H6cxs4+aDG+c +z5ea3hbvWQPUG7NXLeXl0Rz9D7tx/Q6m7VFrduPRMzbrx6I/y2/3Jpplmg2IxT2dHY+TXL9xecj7 +jxe+DD5enH+48WFF5OXunTNPRszJpR0qqBPnnnaqYcFNLd+6clqLp7XXKm3/rMVHK1dH8hkPHp7f +uv36Zfk4/+Tw/OebLzcfGozKjvh3HG2svfEiL12IKIwNfel6HjLLc41PuqVHi3Dlng== + + + z3Vs/1zafv8aZbLPbWdMnjOIs4tPXj8dr4U5LTLcKYChVP5VBbB0uH55uTdBFbAw9WXv4Y25/e1r +Sy8Xt199RwEMdc2PWceRbcSu+v8t6ziyjb2J/z3rOLKNI6n8563jyDYerYD+o4jgiLFPVDyekLX7 +Y4bn/M03cSwG8+hEDMbtz47HYDbK3RNhnIPJ/WEDD08GcS7eHoTleUR6WrBo9+LJeNPsvbFo02Rd +80eiTencmYmF80vHAlYd74tDpJFIWQYivsuz/qfLr6zYiKUZsIAmQ0kDswyU9nr/racj1Mnx24Mj +6b5zF74bwei8cMQwvhfBuPA3IxjTOz/oua7N7PxxxEjjRTw/f8xvbwP65ehpoNP3pn8sYpT3fsRR +1+3o0rnehDYxihjdeH5xXdZq+hbPV6W+GM7Jl+O/XXmy32/GQ0niinMZ5C0nF2Luby7E1HgMDUHB +Ua7yh8KCunPkL/fh2P7dqZm/E5rU/SKnNMB1+eEm/qdRudEv/U6Z7zRx5ATY5XczP8iGM2PT0pv4 +wxaHnRfZyI+n7ncnsn40R++z3+8hBiny8mccM+NGr1I+11fdvrt45L65i2Nd2vGXH3Zd2nbDOoXJ +zaeiuG/sK1QbHqeq+una7eearxgzKGOxjAdbO82wvvh11r1d3pxBxmhWcRvyMYrGBy0j8+wsTc+F +Lnsmn1C/RjusoHtohxV0a/Ij3pjVy9f7W4rG/fkrV97Rgzcf3tz4OGYrxzt6zLSOfXHU9xz74pnd +HH5xzM5+GHvLONyfPX/n0xBAazbZ3l45O0T698eRvtiFIXi7Pzv+xddLYNz7c8Pq1HjfKFheTitY +7vuaGLPv30wM2340rbe8/2ohfI9mhzrpvLtwbRKFWI8GDXKnRbTyyFBliobZWH5AQmt24+Ur20V/ +bk0PE7KKPdxUujScmKcDfYuZOl+M+/Jk4uZMnRLXzKyeP4IKJre73xZTUHqi5HBYGvN07kdbPK29 +3mktmrEW7dy5/SvTzy7vpWvPwv35/HpzsiH0l0/OdNnWV26UZh6xl5s63PyVA6f/8nxmHIgvX4XE +Px80DP7yjlXGX9p40IKiS1+eufbp8NUbYit3c2r9bVd5tJKPJYa63ULi7GqdgkjEPCXwYscWT762 +W0TQOgc3TI4c3KFPekGTKVv/2mJlw5SmflDooJGe6SEWFEG7ceZ1k6dFopmHMwxYzX1+cTBQ3Pp5 +5tI0tYU/f/nmz6ftz/DXV+/fVR7CP71uv8Xk5rOG5Y/VRdDhPlYSQa/72uKsZj+PFkIM8a1IRl5d +Xnz367x4r+OxutuXpsAs9zi3olyKLgkc4KIgWGHs/H9f613uTSDe8/bmzuZ4rKc3MSGUJ1sHh19x +Q3y7sPXx887K+r+39nq2r/838n/8m2vfutJHMMn0I6gr73uT+98+Wm/yVH9FdPnbufm9g6XPGwef +d3fW9/7dvwTSi3srz+4s9S/1R/de7k9Kb8xbuVu+mkKAiffdR7SJD53SyHe/lNb+a/vLjnw9u35w +sPf5/eHB1n5rdH5vb/3EXRufPn/Z3Nva0Xtcf+7OzsHoW/xz8O+vW/rt5KeDg6+X5uZ+//33we9+ +sLv3cc4Z8UBkLFP9uSfyup2PR5/9tv7lsHsY9P1L3793Z3273dq6126e+fvDsn84rD/v+pPV22+f +fvq8f/PLFjjlRwZw4hHS5b+e6c/Lfy9+7x3Knwc9M8jOxlT7ZpBCNpEfTC7By4dY5bssH2wF7ZQP +L9alFTNwZE1p9t9y+bN8+Jfw6e/90L/Xf/Xa9Dfxxsc9HwfWW9+3Ng1CDam/DZLxxZKUQ7R9HwbV ++SKEPDBRbl7rCSmZUkkqNfW9H9RgglyXQQwiC0KQ75VQgin9jZ53A+czmqnyzlD63g68qZYE453r +exlecNpIMlaesXYQbI59a9KgmAJBK9JR6Zp8SkHuMH1XBtnqv9IDko18UevAVQgnpJK34BOf4Yc0 +cCknIcoIZL77SV5asyMhOWv72WDYnvfnQfTG97Md5FgL75HhlX52g+iCDNOhV14eknE79txFWcYa +QYloNQxKkg9ymU3ltQm8PwYb+4vygJc5dXwgRIfu+oGNMiQheJnt/moPM5MMptXIZNYc+lW+qwbD +CBgb/mz0oqxGlFfjKsh7ZJXks8y28E7/xIov9j4ow8QRx8j6xupxkzBD9hlsEWS1bCGpRHkX1ryk +onxiXAJbyOByUrZwKZIvvLeWhGyMLLEb5CTtKeOIsiRf2Jo9l91UQ76ImQwoBOcDCCbF2lopkdMr +zFQzGcN64UALxjBJZrqtcd8JAyQZefeXX2BpqnCDLx7q2419LZ8bd2RyhwPfyboW+QvmcEavk3wv +vFGs9Ah3J2FWeVB4w2MUcu2y53UpDrwi8+VkwoVXnA2h8YYnMzhIC5dtIEIjlylj1uXSBaPfpyF3 +2ApmcAORHSy4fIghgmBzyY07vINAGSvdNxncEWIkKyl3eOWOVDg23w/CftID+SzrHxJl/ejCd9zh +jzCHSbhFGvLSyDbWPcmLhCTjiIG8YUJV3vDRR/CGExURle+SF3UCVnDWkVCicJ8Qgi+RhBpkcZU3 +YmyqBzMlrFACdYQQsFJgFlU0SdlSpldejlmzJg5Clje4fhFhlM7JJ97hsOrFSr/0r9Kd8oYVOe1D +YtPwLnxuvJHAG1bnTxY2Z/BGCMHzWuajgDmGXBqbTIsuKUXvCbKk4A4XnCPBUU2IpjMlt4cgKhli +wLWRSbUyCUIQ0VFC8PpMLI09hHWS13ax8hD3GPje6l1jDoOJM2IoguiBCr2EWXJOecMpbwRLdeLA +G6bGrOq5yriOr/uQNcYUB/qWfKGEiAqHPQFvZelSksk0wlErPcpxFfaQ2Y4xt75QvFRIqUGj8Iew +Aa8CmV0VQPQ0L6I25Vr4I4kdWiSnQK+DD4wRFpTXSFdCreQDiqQHH9QqM+d1OT1MjpfvR3/xBaSk +ihUovNO60df43BghUkl4q9LbmZDgqbmPTQJm6lT4ZxoMAd7rb+xuf9093Nns739a/7rV3xYAMQYS +bH/+a880xNnmWl4ZOcOWM2KlvxU8L39p6prZFU0j3DO8JeOpjZ6oh6T36i1HGKFrxpJH7fDW9s6N +3sL7nmldWfgknZt8tgO0s9n/uLe++RmJUjul30NOwc/jaFke+tgjjBHjJNLb/Z2NVMLyhoVtctfC +AtDR154+7LqBm0EQ80dra2Rht8neAdyZoWa86oMUCq2n3K9wxojYgFBsLUQXYtOwkjmCBNYW9SJW +DryKLq+RVaMRICFra6MoxhXlZ8huNqqBlSQGyqlYgzGFECMMhbClS/0TPV7EBJ42a15nbbaakxMm +ZBmD92JhZ4XzKxZDZg7/szQI2q0A/dR/sd2b5Zde1M1scSHLA+WPHiyCwQbC5TL5G3/tXQt/+V3d +dMqb2jKHxp1YSVkLLKoXtIPmsXTOyuICcpALRQXFHGm8c4VetWK/irxcFIGgEdpAUZLBVlmdIGtR +YGXFpkXwiHRPlHKlZpNVgXoMwgE+K0kQtwVuSoLf2B1q3RP92/izJbTllDWcBTQCtGhz4kYfpOFa +8lj3BORxCFjKYnMj834xCCcfLA6oMgKk6aMB8/s/eePCD75xOP/DBzdGYjumqqIImhc7ETP+YmmH +6jqfVNdpqK7TUF1HVdf2qLom6A1j6jqourZj6jp26jofV9f1uLq2nbo+1t+VH1B3bqpPGZCu80OU +hsgCMMG2wFk6xgnJ6kvarI596L5JYmkT0Sh6MVKJh212O31oZBKz2FEvywE4s90TiyrwTRZKDF6A +bwBsJVMhgmmKwHBh6gCAA31nITYy+wW4UtwDWthEybV8d4KkyTAEivWrCJqIuTxBSmkUvcGJZ4NL +OB68tu1a3L7FHv1IIkBwqVicJPKYIk2OIoxSRaPCBpkCu45uiaRbqPEKSBwTgY3gpMq5gRfJwYOA +wdcELKCkoBSrd+ADp6d0hLH5ojaW2RcPEjpAMLARbp8V7ZYxJOgW6bn7rqVzzdRhUQbihQOSzMKj +Kt2HLOPCp5qS+L4y76JniIKFQ4DMkjCo8ESgWsQoRdGlRA1aqtgN+ULWK2HUAE4JENGOccSfBZe+ +rh98cjKqt+7t6QGmSUM3qNaxUMbCwvzGxuH2492DdTTboivjL+nP3d89eLy1sbu3KROCrzn+3Ebf +DT50Qz994HOPt9a/3FuXt/4Xgi+Ti/N3bre5ffphd29bvxoGUuY3d99vvZ2/UzGAJwf//rL1dtQn +vSs3XIWYyxOgqafD2E6LuNj+i81Oqk9daPcnC33CNssqpP7kVP/Fc2GSpR7UbGnfi4wWYVd4XLBL +IgQwb0mUjEMIQoTUQhqFHVNGCMIgFmEYt/Byg4hFKA6cLa6BqNsAHEsPQDSXWC25ttKCsK6DkRIp +zjCCBp6BiFYlLpL3VxFLyHcSGYiIcgDpgNPg4YpwGg9UJAIrYq2OZSnaYQg4fWDgcSEIhHGqLj3d +D+MHmLYiklOEUuEiVf6t6GaB5wB8Zbz6QYuYD9EerTWYciFkhAxAgOyfmLDFH8GcVrWwuEUe6DL4 +HKEkraxvMNKYGDSofrBlokl0yVUYNJEnmZVZL98LYoOXlSw0mCwLoKrMLyMQXpYM8R76RqLoI7Sb +zIVMo6ylhXzC1vxVwcz+f10wdejHRv6H4/5Pi2Vb51MF8c/W+Q+E8+IhvdY4kk3T/0ibEWETECqA +975NUnaijRFZQmhwhaTqLUlOjA8oRaOZRaCCiXoTWFpmVZwoL8ayL+AkakDCQjsGCLSsqEavQqKh +ExKhA0gRoZNSBKUgdGYQchKfvCCIifipYbwNRrsUjTjwnmDYeeerCg2FGIqCTjulCmZbKBpdAyVU +6pImaB5BkipeD4UTyMjAXXEA0YVBSI08FoQ9SKLTw2EJ6AcpaDQDd3FWSKKWwF1ifZOSvA6NgMtq +WzkimAiSY+vilDJOiedCSEoaxrd88EVJorItwxoFEXI2JegikyTG1utdAob1riBLKUhZuseJA+8Y +eADFKgAiKciwCvrrdXiEqCQUfb04kA6TaRvCkA6VwmdSIoYFsGLcqyBIo604BAilN7CFRV+eXIID +iXYEJfWrAJJcvVICQhxCScHbdo/wV23QTUcF5kE8k0MqwkG4KlGW9Rsmwsrig1LFh4VG5eKgBad9 +Q1hF7mUcxrT5tN6yTZkw2yYdKk2Y2Cedc1lusUfQ7ikZpxRBkdT3zhPMF4xWFL1QGPwkpRjhwMrY +j8dUCRtVXBfRpKvarMx7BUYVY4yJ85HXwjZZp5JMVSE+qehUwrMTgnhkVgkBwSbMrYMYk0SQB1IG +dNW7EIXDoohHiJayQE/X2M3kBA7MMk8lcP5Srk5vYvgdswP2IaXUxNkRmNPuiUXHFaphOxgheil/ +ncNcZNjFyKeS15Yh5HpLZkghi1RCgOVVgJF6C8ExXmUjbXzUtwjjfGMLISgFXCHXTA== + + + OGAF8PY1NiALDwraYYtMMFQGVyHlAoT0raLxileCaDWGiozwo1KsB86X1orhQ7DcmKekIUwEuiEn +FSkPkQ55syytEeGsfJatWPTbkWKpWhE6QUJGKNHpu22bSYEmNVuleOAMRKeSLhuMAJ4SVWtLo1Rr +GVP0YCxSLPSUUJwKF0ORIusV8Q4XlSKGT55qoi2Uou6iUBg2EorMm5F3VEhaTnpPsZhz0VXi5DFs +xCCcE80o4/2Gdp0rpGSrIdCa2Lcg/LOG71OwpAgbedwRZdI4L7lrgbknofgiksl3iNHDtS9YV4Qd +RVXSRYpUw9JTseAVsyKwFj6wcDAEJ6h6geMV9C0Z4aqNnlCC1Rv4QFCcVhnPr/Q3EgaKBmLQB6x4 +lKTUdgcyYlgai1wQPBV4e+gDblgBxSGOLY8kUdqgCPiMhc3WTPlNyHiQLQSkFCUYJYjy4DNQsfK0 +zFhJlFN4y0gxCiULWyjFlkAesJVWKIqGiDHp2omcRpFpeTWMKqV0rQcKwHdBnBxPCPYWqYBdL1xs +xAJgdAoyD5GUpN0WMOCp4+QWgYmwqNGQZYQQo2f+oesIUokisDCLIcejFOtpWOXVTsYGhV0M1bJQ +MkI4HWrmgMiwJajXSwqVeIG4UPMlBCo9oXUzl0IpXp4XpV5LbZNnoGVL8wNIcR4+hixXoUrDGhio +bEROTWyUEgjsYwrtqWSrIpBEa42QA+IDQhFL4AGRabgAsMAQ33AHlxBgCooZix+kfbmOVcXDafBW +KAkefSLA4zVN0CoXn65H1jnD0iOHVlqmQK4DRoplFa0kNjtZNZQFCyyzg34hgygILELF8a0xy4JW +08WmAO8j4BRTh0V7GhnckIlBalYoTCqKuiB6SE2ga0sBsFVx6fkeIwiTnhfuAD8VjA0OD/graQR7 +EQ6iQ7K0IPltKLbVA49mNaprEHSmuoQSgmmin43lfIiIN4oDtMIMIf++AtktIbNdcRKpEAMDtt2r +U1B9gRcLRsTwgsbKsBDIj0lnS+RaFm8BJRLy2cKHQJMi79om01a4J9juLXA6hCISZJXiRNGQRyzN +CxqGagQfxSFFdCF5zarahaRE8qPzrt3DFDSwX0mNwkgo72mKRQSGfJ5MW1PhlNJXENRUS8jgWDFg +WaWlJjEBuHa5SZR0jjiPeQJKnTjkfMbA9SVFrB5lTJioSSahJdzgyt5EBJ4zHWZjTZP5hEAPgDCy +onJdQ+YYPUzGN0p8LQR6go9CH/JuMoEUBG6NNxSsEeoqKLjSSBCV2AdMcrUpMdHKBRSkz3EJ+AAY +gzVaxSMGGlgoYrcc7sgsVKhYDzgH0INoLNfhaJDtR3lEZYoRl8DkGcIi07fKR8BHGVEsWTAoziqi +kIumieWaWV1cA3dsQLWKkpcnMuJGvIP5J1zDuq/hjoAJEgpzh9INYaIqoiYUa7igEdnW2ic0UgL8 +Lc9mZVq0W0hi0ENBv4WHce3hSiyi36YWUizmOMqd0TGYUsAyawgUi9PHso6I4psIhm5FHEgKr+IO +rR0BHpEpjrR1CShJtI/VNlIUJJXpTETcUaDSgJFi1iYIgIRgnSxwRM5Y9AASUILt0ETU8phslIM5 +UqsZSOHKivkjKBQoBO+f0ydIoPIJF5uNiajuyYgUoBso5eGlgLCKS/FDWJMgTgTLTFBuYFgEkxlQ +QueR4BKcTwMLaIuJqEzMCHvSg8xelxDsity+TG4QVK0mWEwZs0pVRL2fAO7AaaIvHLJxME0G2XMx +p/IOyKcHk0QF36uQTxmex4qL4lbtX4mFhWPVfAR9ng4rjUHVFggqYAsK0ujAOQjEA6DoNTXlIkGM +Q2IvgteJlEK7g0xPTUg9jgXPRNXARrCxYKNqO91YrXIaMTQI7Bl4QVVy1MQjKN41CqAV+tK0IMIe +nIzcaXFrIbtxaA1QHiTaOI+0MgqURMQwBbmz07lgRAlMbjVVgBFBaoTLVtXaOwqfbTFD2rrMKh+r +YI/aFRPtPdEgC0KQWI2Gd0SmMjLUuo0NTYfC5ffNPCQmEjBLkZ58yohqJPJQyY1CbZhDp9Bg72BB +MEI4aXgGKApvRowBiUYWRkR4kfDZU1EfB5SKRAIyfoZMQtRGQJ0gZ7mFYnANH17G61GowjsK51X0 +SGGSEfdgbTP8NUZMmEMWlZBL59CAUqhamV9ILWEB19Nqs1WzNdCcGRMMBscSF9UM9KQCqmiKUU2J +yWJXAWaoTSBe1IGlcFYhkOxo561ZhQNCSaa5PiUiUQ73zbV+elT1YTWzIoY6TJRXr9iyUDll6NzQ +CFFAA0UHhgNj4WilP8g8rcEPK+hDpsik5uGZSh4gC0PXIGAGYUKdnXi1LR5BLnFM7wsAVAawCYsF +Sk4sjLLJsFUUWqWi1Xq+jVkTfA4gJTQvEQASyq4qI2UUEFILyzhqx2yBaUFHFI4MIm8RXO1Du8Wi +YQw2hsaPyhkoaVC0wog7tbWwbm76ICcWEAiELyqVdMiEEm3zlaw1Zbx/gONw+6U3OTYAwyQRRmVL +83sYhcac4F7q7ECzgNrINbXUXhfHdnYCAL7jPBJiVT/TArjSimarJRDG50YhEEVysCgE0HJG0UDS +tRiUSaFuCxU3LBg8Iyhw8Thp4qLaAFZ+0oI5JLhgA8TVoY2jDSqwcN/4PZLC8IQqDBrcSsM7vGJh +gohETOMyFxnumLitMH7R0jGU9xGTytNWfHi8v4jbwNbwzCodRSTOASaoP5CKhT1DRKRUxfFE5RFZ +2aEbi+oVvE+DGUzTGfaFGc625sBTST0wUmiE8FTzdSGBlfewmha6TdRrTBqtWAW7GUQHsGiIH6cW +44koZk1cD64i4EBGsBcP0JZHZJDYYkUZSoT9capxiJ4iIG8XCUke02hZYoVL+Jmx+TUMSNAFEAqD +h8r7coeIN8JLuC5oUqREfGxUGhmNIguFATUGPYR9QtXMErshIhey8o0qqSKKIKAGxTQKIxIBPjFl +Jged0QAlYpVAAxbgAoZ2Cx3WAJCjFCTOpAtC4YIDOgjOxnUyZICMBLgwWDDK49CkgmDQKh3cNcQF +ufAoStNIOMExSkayRjrkWgSmsvsyYqUg8BKQVjctTGiN4RwwN61xSoT1ZV75lxTnMZNe0Yg8g+Rv +9FpIt8pWETMDBT2SnhWveNohis6QqahwiqINCE1YZhQJpOQdjLqGDKxlNc7ICHYcZtdxib9wsKBL +N/AAHVq4ZVm9DfFtUDcnopTAU7BvqIilU5a4MqXpC6/+CCnkO7rJNDHA/rGgzhJG1nLewdu49lyY +3Jg+qK8IfMLaXOTCabexmIYlC7ExjVMRAwNyRrPXsHQ2KmlkkQCfFBYulayGJZKrDQMMhFeG2o+r +mxsEge6DT7PG+C7CK4Q4to2OfgNdYNM4wnBOEPtq4XD4SnDFoQjhhCLIDSuEpWYOhmVGMLRV0x9e +q7OgmWF0NAOTSKA7At+xUgBLlwBgGBAS6Yr6lkhB0MpEestcXFyXGDQrG72Wtgmwo+dLbUgzVKx6 +woFGyCR9QqQP9eWgICkM9hCVjWvqMGVBIjSkURJdT4aF8Q7PwmWE4xF2yEhKFrqrskjsF7PbFLlI +i+kUPgJlIVXFAunYuINimpGe8p6uJhwwoPBIvZ2bLQEggskDU/J+VjJRmLjvARSU8pdmr/FO2CzN +PxBsydjUy2JSo7CVYDWTg1pKLQdPjsyAiEjQQm0WkAklaRkbK3xZEYvkIarh8VTRxc6qzBDyRgCU +lJIUNWTY8BWk/8TFIrKoGg5HQrA4rg/jjqRUzoPRGnqkHqHeO55ZRebRQ3ChlYFya9PbwDbwhRAS +R8EGjU1CRThiXKgq7iB8depTgJkN1U516tOlrEVBK6BQacLgWEZXGBI3yp3CpqAgQOyq7rygn4TC +YK00sEhv1s7BQbotIOyGQDGUABFeZFScMf5MRuUT6mUIW2QDNxRZCZbjw2WJTNI6KldsvwiMXxv1 +UsmdLIXtHHhW5yMVgugk7/BAHkiNIUKFFa5EiszRFAVrluqtikrIOhSWHJJC2G21sGoFO1ayV20V +US6Ea4bWKxgXLFqr1rgjqwA3i8XcQByAZgldAcUP/R5UniJZSa0hnczcpYHkLKvLggYgNctbqMCK +yhuzvGQKBExiy8KiPAP+lY0sjUVGM3h1uTRnwuQZsthUWijlN1AtvOb2j9We1p/DeYcL1RJ1QB4E +Gt62MThVSy5ZLVn3SJxhYqBBtBVfMwvrc8i2y6Xy2tIYYAjal9r5X7prhr2pKWgu26O+H33JQJmk +IJeADiOcDEJClpzPZK10pMjTvSssIzZO45ogcdcD7kEQExWGNeic+2ZGWrxQywQKnSqrAsekeXK6 +iSixDM6qpQevZsd6OGNb2CvQPnFILR4hnJDbwEUr0GfiBp0VfbvVgIP4lZlLblPW0FfkwDvZCyg0 +0zFonEoIpSiBYAMEGHMdeMTz8M5y0fpRsg1jPEanT6CxFl2nmlqtEGOeIDG9LoPT8IlRXkwtytGC +cKxLwJ3sCbkeRTaZURub6Fpy3ej5tLSUTi9tPGJBxeqMF1bnQLN7lhpFzUhBadekTI0ATPIkmVJa +QwINmD838HDxmIB7Emps70eILjEdw+057f3wlZE16UjyAFxshLK7gpKolgw1Jq69D0tpeVfRRC3q +O1jTAFtSiGRYJ2I1BB+cFqajmgRIHPF1h7pKEjQjhIz6N70lKSnnoLdkZnsAVUrhWHKbFTBL6Mpe +qCQLlqARkC8EPqi6p0PmlsuKgHXuZjsiN1taXYmSEjLOaAcKS1vKqNxmwN/XRjpSirPyve0TP1wb +1aqLPvYCkHnSFBPyids91Kqz3CMr5FnpBVSbM9OBfCAqw+HQMynlkZEVCuvJQcGIUSrOXKJcQ+Gs +4Q7RM5oIDAyyhKR1AlgKwQdwdQT+6lKBFb+hH5qgwOoXVnwK02sbiACsnej7Pzgt2Egjpod1bAQp +20oCukBMFK4RCIr0Ge9d5R26NwJ5PCgjELBhj0FG7qdwtN+pbV5b1GfAtiCltoVHwUSrVlk72Zl/ +cqCmalaTUU9Be9tkSgUiTndsssgL8s2MpVX1HYfBUaZDVL4ZiWCeMzbB1dyf1agxCBw+THyi2Yoa +nUQOEpWf8mqG5VIrRMDgreoIZstR34wZQxQczhMCaW3is96jBdQgFM5hqr5dM4jjNaCmEy+ASO8p +NnGvi4Z14JTB1KHuz1iuHuslFjlbdGAxRu5xMy2VhSVHsOXEfH53q9P/bK24WTTCrwuVa1U1bYHw +TOHWENHlhTmTtkfZ6O48pl2q7iGWN8unGHTLCdqIWQkRLssab/HA1rFtilshqbjCuwi7MV/MusC5 +xl8QIpQygjxIAut8demqZK3qN5BiI7GAriqbxchKtxOj3PgHtR1qaSqT0tRQ26QgyU2ALvMY4N0W +1sbI0idu+gNHVRotLCfiINBYRNrCN4EpEVbcsOAjeE3N18SQ0iJeweBDBQaM3EnDag== + + + f1zDIwxw+VFAVHQpN7D1R0uVkBEy7FSyRYFfqcSsuKWhxVSYOkY/UtWbWmFdcBqS0DI/Qa1CSNXU +ITpExKYavYEROw6v1Kzgy6NOyhs1UbUVf2LvN4wpqqYMc0Yuafi3thItFmUbVrEwKYwtnShYRE0P +ak74BFmv+hbexT4dAA54TpkhVdfySaUq8BQKdp5pjQHjayvYNCMzykpQ3ZDpNCZW2v6GVWzEDSUE +3Qudqu6/Rr0HHCcaEZ8UbiPjaxms8m2XNaJLGv/3GcWLgElMZPmsLiRCFi7BZcQj2LeJ2A/qu33V +RAEDDgShgU6kFoQzKI+gXKvkiATjwStSh4OKygKwDVLSKHbHcFfBSCxVFwrdZfCutXRQGcQ8zt3/ +pOBUlApaGGGGErexM4MAM6CYkghMTDpza6GoC7oCsy9DYUyPm/JD0Ix9wEajGJWNqSVD7SLSQmH+ +FYXkgUVuARvHZdqj1eifbmCDBYjD4jSPXjBGS+xEAksiEcfViJNHYF8Dt5wtUiySgKAUIh0fldcj +zA/jlD7orvVodfckKRn7eRDeRWyRFKBKvpyRS5wGIb4lm4lEdb7B4OjUTVsBZ6JGCoPSGKn3GraT +dmNm3TTYO3MmWLlBAvxAIQR9sVVvF6vDNoHjbeYNmRVZrmpINkK90e92rQ4mqutHgjg4zMO37cjY +sS9dQupeq0kcN/biEYYAHHeHcHipMFyN3dZAYthKj6iqc1rZifICT3+vO8wBSW5shgIQ8JqtQOng +GuQ9YnNtrAp7HQAhxlIY819DPzlhVZ1sHFNRC/MfdO4cImCi4nA/wLoomaJpr9jgzPgd3WX7GgiG +DzDoFDPzyjJ3AS4lqiIySywwv4LNuFcE2XF5IKJkAYPC3sg1bQJRHaPGYIW9QFITlWYw0NKGVRBZ +s85d1Zg8UtMwyzh/AfksFBBlWmNQbGFEWVxG7tG22O2EeAUcT96RMDnIiAcmW71pGf5WAI8jI3JJ +Gr9IpQ3GB0ZYgzAcTxvIuhOVKU2eUJBqYHiAaSPldI1DMlZPgqkauxRBz8rGnV/PUnycahBs1kJO +S53cCqgZh6i8g4XUCGpV7Rpi8Lrj32F3GIeTIUmpZaPI+5oARYSwvbjibAeGFcnHvlUTYMdzZYgJ +Z30wHIjAL30rULDvEr609U14vU3M25bKIhmPPIoWRfCMAh0kfCMUPSAM7K1iArjW0XgdQkasBhuP +cJqGroiPmmemX+tb7T12laIaQhYxMsVcbFZW0o1KOB7CNlZibhkVPtZ2kspIABkxqSHNLjFb0LQc +T2FI7Am3nZPiWV0ET1nNL5RC0NoKwV4Qz5A95zIl+iA41SN6hrVZ8OdSK4HACTwsRqeVN7rH31FJ +aLzZaZyMKoA1XmjUMUHlcGqIbkmnn0CKbVUyDQdge1zV+E4rTbZA2I7xkhYnddxURw5iwZOlY8gb +cGyGOAgo6wpaIg6L6ZCMLpwlqtY1NMo0Wh4WDlqyGvMbTFjKM8IyrBzgwTrOtqR11RisDBBRWc/a +A6qoFVC43wvPRFW/TjdGaACf64NdIwg9YY8bwyLYtMYqq9ol+3B4BauqoCvIPM42tDA0DtirLp4H +KAx0kpItEERLMmt/YtFqPR0UcreFwAXukXMajC5OixdX2UYL/zApi/5j+yvzM7WNmqFNhD40SI0t +dqh0RlADBgy8YmorzGOFtmO6mckRGnunlTzIcwRVeUnP6chVjwkgqzNGiSyFDc22BaA95DGARKBI +URXKOwwys1SURZ9BaxAgo9MYXWz6mmXlzCTSs3YAV1oBKH98ew+KsgtYnelNl7UMBTtQNJrtUAqA +WlZMC9kHKrnNtU/0fHCaExJ6pcUe1fQGrdvMoZPDrCXL2TNS6ZKamq5gGrwhSIyhoDS0rJDv0rZD +cJUTYAGqVRFatQSyWlSKO8jtuqpRdQy53fvMzU8W0AUb2pE5R/wuYWNQUQ8bCTFAa/HtijolhWfq +JNzB3U/cGFW4iAZJDybNgkJECCpCBFjo7Jp4V6tLryclySImxdoVO1K+UTIBzHWzi8cdES44uYml +VBR3zbw2jIZDC1h8207UWkFvWViBvngNGDa3jKHZiB2MhNswsgA12IPaagthp2W82LkDt9SrJlyB +ViloXSi0KnxL9C4rRaOuUXuLCpFI+2KjKkb4v841SsEmPVaaKINZPWxI2jFw2jHmgiIPp9gQ+gvy +CoAJRMbN5xbnoIGCVcE15iI6TdrhuiiGY5GAblfHoVhEtplbZyPSfQDVGnqBlKJoDZUPjqoXfhty +JyhcQHYJ4gM3CqUNVTGFKNwEF4A1A7CLNRDeJ2PaiViQpdAOmRLbX3CkRkhaNUjQEbEHGpQAsIPY +l0hNgD9ImcUzIlDiTDSQi0dQlh7anjlSGHaAA9yED3xceXAOs/U+avLcN1iyBo+B7gtcCc3Je+wc +g9NYNDDkq8b7fEuByDNV/XEcuqSQPBgttIRoq5OKWgtAFlA8uSS0bQidiaSLwypHuMFA5cHpnkTI +NtYGZ20g0SPX+HqRfhM0uENYItD1o3+Lg7KQMaZnRZMnFNZN07Pihhksa2LFDU80EmHnqUyWwQ5W +lhPvM33GduHCZ6gfthJQchpB4QSq44r4DRA5ysNIoRpzRXOHfAqOPgBzYaQWriDiLFSu6j4C8Gce +ZYE0IMLMKGgDFqSFDq0kCQwRKeUIiwAHe27MJsuw6Nc3fbNGlxRVBAChCMqFrPvU/LAGL7TNY143 +WQeesyCdSBrJ5Q2YUN8OcGM/WTrkUSYZ6GxhtygYCQpyrRcRO5MuB+6RBqvznC+MmEApGo2mhqRC +vQIKvWocaoBNYCsn/O1/MOILDW6otHLJiPfCHqHqAb5p9C1nz6x7pyxKy8NFq9ujWCVDVxBVUNhI +ijoIeG1wZ+EDsy7C00OnolrkRkpwENxmOFl4B4vMDWwyAQXTnaHZXz7B0kYoD68bOjlJ4BjrdLdm +gjZCCMJrsYEn8AtZi7xJ0GIlZGm1agCRn0hWoJ4hhTFORCnikMKwADVOaa4Bxgq4vMpdvWCnaBU0 +4g7Uk9CnZoyHpe9OC2M22F5T3zz9D3ud2oyzbmJNF0XL5dt6H1umfzTkH7S8hDVMGvFHlb4WErnI +fCK3NIOQomZHmRRDiTWyl5qEjEWdUFEvSZOQ3AmDXJ7VzCXTdij6EUvXpTpD57haxhjpQKcWIWT2 +VjfPEchrfpfRUZR/+6z5e2aKmJwPscuPY5Lg3BYGNXDYpdNzC1uhG0oF4NZhh1DW2CorMXB6nfFM +BRqN+4Hi2/5Yandu9ms7YWEh2f0aW+9YN4liLx6Yhe3asJksRo+6y1sLB+DOa+YBiXgsNuvBou4q +93gaOYPYErosTOq2iC1yicS+aw5Iexda8WBLyjD/EpmRSS23hIQ6sh2sT2f+M+hGstTW6jgr/IMR +Q6ZIgQGQ8zc8FREkrbVBzMYFPXeG6RRgR6eFDFnDYKkokmZCNmsJBc3XWkvaspZGa4xIYGFX7qof +UDmK0EXWMIhmelkjk9reGuaQwYxZg5Ka1KI885aiSa2o5VtgJB81hUWPpDsH6JvmwsTR4vl3Ner5 +ejwpEPe0/LixWo5KDbOh+fFW0uq905UocO7H2bqlwkFCcT/P9tQtFowZ6kyQGyB0iPwxo1+U/+iv +rTGdF1DanaLaNhI4W1FLHL/xHoIfCF6I2k4rHaT88BqIBARglo2Wu8Y+RqxG0ESOHRJqbu+u3nAm +mNvgQ17r+oSd6Zvj3TDxCYmFrO8ihtX6atvaYXoLhSmmau3AMf76BzUktiMzJMfNGXp6lIdBoSag +/11R/JRYIuQjzwXgXu7CoXIzH7IdRleYpQKrfAZxpJR0oLVtPeFc8JyJYy/+RxPa2PWvR/byXJtt +JUH/dOV7zBVaPZnVWIpIbYf0Ot1doRk9WlKSXEvfISzPfVA4Y8xoVS6uPQ/pYcIRGT0ezRo141hT +K1fkkZ4gRF7n2r1GNxp53TYAQneeIbc3aCrWR91tlfQhAPXEVyfNbAJQZbZik3KWx0ZPFEG2NLBp +p9chBoWYI+s2Cqvb9Gg9Hr2lW96qD+0Rp8Ey6hjeAfSO+GC2mi/vtlWxNklP4CIH5S7J2R1aAKGM ++kxA1AJFSqElT7NuyOVpTLSOeTgcbn8BgSdOcC9dy1BjL2xwwzpO7b9tsbzQSZNWTlr1EY4QwLOr +7Z623Y3HHGPhq+4Q8bXVzVXVFrgnqZstS52q7lyhJ6gJXz0K2mgYVe/KNM9VM85KotJAPRiWCNlk +LWpCahIJSQtnXreAYN/dIksDAvZGICiJwmrmqJGz5L6ZrIQjXP9PZuMpLYp1GUzWbDxBLQJs1Xaj +18NG2vQzQQ+YgfNJrNHyBbjzQXeyxqQiRNCAzboaPGVdATQocGGpuh70ekAAkP2m1Qn4jlXLSYXG +ceOtHsqNlWiJS3pqKmdaImW706YqbCvPX8naE54fwjLmHNsjrAjHPZlOLwUc7gqiK6Xjg24zcYSU +khAa7kdO+8T0/UNLwzVwLLsS/i9tVbh9i4cFtFqVVjZR2rmZjeR1U3+2bbYYCYIPEa1OzXjL3z2y +NB85/S6K+dTDQN3wb8tge+4DQPwgM7CCo5lq0c6CSwyA7yxj1dhkPYsd61B1CF1Zp8cCxiry82K7 +J0oxI2iIM72kl6c/OJszKzq8nirJAs6FDRZSIbiIBwx0+Y+8c2EbCVTUcGFQGVHUUx5DMY1pR7gl +cafwGCpvkPjhLhjsrT7tdazbZe1C0eOYFn5wjCjsYTRDnxQ2H53B+Q/w18Vnf7OVPz8v8K9wzH/y +TLLTDiLLP3Yg4Oj0YxyGip2X6kHyCPNtpZh23pZlCgTJ5ty8MNYQhrZ9GqcRJYYcuZOvtp11iwjU +IOCCcn0UxSK4YrVukUUiCGY0FWAjT+9GJjipTdZNHa7BiXaIMTQ2a6yDZt0WuQUBNVHcI4XzZawq +OZyU5DKripgxRFog81huIsnM8laLM19Q8FK0qIXnCvFQM9b1B/q/xbaz+eE04shASEAZui+CRY/P +HtXSLDI8PDofETixNrMWG5oCD7oTacLpIn9wZOBIfQmW0JNYHWwuStNNUrasSDLNFu7cxglfPncV +XbLYOJccmfJZlFzjRDO4MDjUw3GrKLOEPKWkaMHAn5wPOIlT+sQjrvlvSxdCd0dG88dj+U+f+Xf6 +un3vCMAfEb3gtH4WPoYlbCHF+yGFlTHcuIGaBB/0PHRso6tauIy6k1hZoceNtos9nntsNZoH4OWt +Oj0op0NwNCrXB6O5oRbArl5/4sSiOiIhnYUfCdGMPU/rRRJNDyBGLrTw5w4YgudRbmJNkL5HCJBn +oYSkJ9zWosf6Fbj7SBPgQEskslidVLWIAikoZB64N5cYHRjRawDLIRWF0Rl1MFn+cHzi2iG5AmRp +Oh3q+cV0Mj5teMa3TAUOpf2OnNUjMKGyHATNUNy8L0CNswh2ibTPOsHAuqEfyqrtjg== + + + lqVFZhg7mnE4OX7FAPEOZMtQg4d4LCKh6HvWWpb/f3KGAR0dzp8M5j8uaact3KmCVn9M0J716olP +Kwv6w1k3dzb5s1mzs7j2+kNai3u7X/d7h/tbe5sykv4cvtjZBfXe+t6v+/1fd3Z/3+nv7B70/8/4 +TyTh+M4l+e9xD95Pwf5zfDBEWBnXcXj0+OSrx/Km/YO93ddT7PiLl0cPJu9+GgnngZ74caRZgX5Q +lyiHnxXRaxcrvSc9/J2t/IYfoUH4xWjsh/o8fzxo2IBedS1YPqZNWBRzn2hCVPNYGxZxr7E2rCtj +rbSrP2injUWbGY5Fn2ujaRenjGf06f/2P3/QdZR11VWcmHi4/nHr6d765y9be72P++vftvrrOzuQ +oa2v8o2w09b+we7eVn//0+7voMgj3e0TEzcf3Or9P39xpbo= + + +