diff --git a/.gitignore b/.gitignore index 06fa83f..5d2ec9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -Debug -Release -.vs -*.APS -cmake-build-*/ +.vscode/ .idea/ -/build/ \ No newline at end of file +build/ +HMCL/target/ +HMCL/Cargo.lock +HMCL/logs/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 321ea06..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.16) -project(HMCLauncher - LANGUAGES CXX - VERSION 3.7.0.1 -) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -set(CMAKE_WIN32_EXECUTABLE ON) - -set(HMCL_EXPECTED_JAVA_MAJOR_VERSION 17) - -add_compile_definitions(UNICODE _UNICODE) -add_compile_definitions(WINVER=0x0601 _WIN32_WINNT=0x0601) # Windows 7 - -if (MSVC) - add_compile_options(/utf-8 /W4 /MT) - add_link_options(/ENTRY:wWinMainCRTStartup) -else () - add_compile_options(-municode -Wall -Wextra -Wpedantic) - add_link_options(-municode -static) -endif () - -include_directories(${CMAKE_CURRENT_BINARY_DIR}) - -configure_file(HMCL.ico ${CMAKE_CURRENT_BINARY_DIR}/HMCL.ico COPYONLY) -configure_file(HMCL/HMCL.rc.in ${CMAKE_CURRENT_BINARY_DIR}/HMCL.rc @ONLY) -configure_file(HMCL/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY) - -add_executable(HMCLauncher WIN32 - ${CMAKE_CURRENT_BINARY_DIR}/HMCL.rc - HMCL/main.cpp - HMCL/platform.cpp HMCL/platform.h - HMCL/i18n.cpp HMCL/i18n.h - HMCL/java.cpp HMCL/java.h - HMCL/path.cpp HMCL/path.h - HMCL/debug.cpp HMCL/debug.h -) -target_link_libraries(HMCLauncher PRIVATE shlwapi version) diff --git a/HMCL/Cargo.toml b/HMCL/Cargo.toml new file mode 100644 index 0000000..d210e82 --- /dev/null +++ b/HMCL/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "hmcl-launcher" +edition = "2021" +version = "3.7.1" +description = "Hello Minecraft! Launcher For Windows" +license = "MIT" + +[dependencies] +windows = { version = "0.62.2", features = [ + "Win32", + "Win32_Storage", + "Win32_Globalization", + "Win32_System", + "Win32_System_Registry", + "Win32_Storage_FileSystem", + "Win32_UI", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Console", + "Win32_UI_Shell", + "Win32_System_SystemInformation" +] } +once_cell = "1.21.3" +anyhow = "1.0.101" + +[build-dependencies] +winres = "0.1" + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +panic = "abort" +strip = true +debug = false +overflow-checks = false + +[profile.dev] +opt-level = 0 \ No newline at end of file diff --git a/HMCL/HMCL.rc.in b/HMCL/HMCL.rc.in deleted file mode 100644 index 05c5d79..0000000 --- a/HMCL/HMCL.rc.in +++ /dev/null @@ -1,72 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#include -#include "windows.h" - -#define IDC_MYICON 2 -#define IDD_HMCL_DIALOG 102 -#define IDS_APP_TITLE 103 -#define IDM_ABOUT 104 -#define IDI_HMCL 107 -#define IDC_HMCL 109 -#define IDR_MAINFRAME 128 -#define ID_SCRIPT_DOWNLOAD_JAVA 160 -#define IDC_STATIC -1 - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_HMCL ICON "HMCL.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -VS_VERSION_INFO VERSIONINFO - FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@ - PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@ - FILEFLAGSMASK 0x3fL - FILEFLAGS 0x0L - FILEOS 0x40004L - FILETYPE 0x1L - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "080404b0" - BEGIN - VALUE "CompanyName", "huanghongxun" - VALUE "FileDescription", "Hello Minecraft! Launcher For Windows" - VALUE "FileVersion", "@PROJECT_VERSION@" - VALUE "InternalName", "HMCL.exe" - VALUE "LegalCopyright", "Copyright (C) 2025 huangyuhui" - VALUE "OriginalFilename", "HMCL.exe" - VALUE "ProductName", "Hello Minecraft! Launcher" - VALUE "ProductVersion", "@PROJECT_VERSION@" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x804, 1200 - END -END - - -///////////////////////////////////////////////////////////////////////////// -// -// String Table -// - -STRINGTABLE -BEGIN - IDS_APP_TITLE "HMCL" - IDC_HMCL "HMCL" -END - - diff --git a/HMCL/build.rs b/HMCL/build.rs new file mode 100644 index 0000000..8a708f9 --- /dev/null +++ b/HMCL/build.rs @@ -0,0 +1,12 @@ +fn main() { + let mut res = winres::WindowsResource::new(); + res.set_icon("resources/HMCL.ico") + .set("FileVersion", "3.7.1") + .set("ProductVersion", "3.7.1") + .set("CompanyName", "huanghongxun") + .set("FileDescription", "Hello Minecraft! Launcher") + .set("LegalCopyright", "Copyright (C) 2025 huangyuhui") + .set("ProductName", "Hello Minecraft! Launcher") + .set("OriginalFilename", "HMCL.exe"); + res.compile().unwrap(); +} diff --git a/HMCL/config.h.in b/HMCL/config.h.in deleted file mode 100644 index 2b71457..0000000 --- a/HMCL/config.h.in +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#define HMCL_LAUNCHER_VERSION L"@PROJECT_VERSION@" -#define HMCL_EXPECTED_JAVA_MAJOR_VERSION @HMCL_EXPECTED_JAVA_MAJOR_VERSION@ -#define HMCL_EXPECTED_JAVA_MAJOR_VERSION_STR "@HMCL_EXPECTED_JAVA_MAJOR_VERSION@" - diff --git a/HMCL/debug.cpp b/HMCL/debug.cpp deleted file mode 100644 index 0b536da..0000000 --- a/HMCL/debug.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include - -#include "debug.h" - -static HANDLE debugLoggerHandle = nullptr; -static bool consoleAllocated = false; - -bool HLVerboseOutput = false; - -bool HLAttachConsole(bool force) { - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - FILE *stream = nullptr; - freopen_s(&stream, "CONOUT$", "w", stdout); - freopen_s(&stream, "CONOUT$", "w", stderr); - wprintf(L"\n"); - consoleAllocated = true; - return true; - } else { - return false; - } -} - -void HLStartDebugLogger(const HLPath &hmclCurrentDir) { - // TODO: Make directories if not exist - for (int i = 0; i < 9; ++i) { - HLPath path = hmclCurrentDir / L"logs\\hmclauncher.log"; - if (i > 0) { - path.path.push_back(L'.'); - path.path.push_back(static_cast(L'0' + i)); - } - - auto handle = CreateFileW(path.path.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, nullptr); - - if (handle != INVALID_HANDLE_VALUE) { - debugLoggerHandle = handle; - return; - } - } -} - -void HLDebugLog(const std::wstring &message) { - SYSTEMTIME time; - GetLocalTime(&time); - wprintf(L"[%02d:%02d:%02d] [HMCLauncher] %ls\n", time.wHour, time.wMinute, time.wSecond, message.c_str()); -} \ No newline at end of file diff --git a/HMCL/debug.h b/HMCL/debug.h deleted file mode 100644 index fde08b1..0000000 --- a/HMCL/debug.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "path.h" - -extern bool HLVerboseOutput; - -bool HLAttachConsole(bool force = false); - -void HLStartDebugLogger(const HLPath &hmclCurrentDir); - -void HLDebugLog(const std::wstring &message); - -#define HLDebugLogVerbose(message) \ - do { \ - if (HLVerboseOutput) { \ - HLDebugLog(message); \ - } \ - } while (0) \ No newline at end of file diff --git a/HMCL/i18n.cpp b/HMCL/i18n.cpp deleted file mode 100644 index a9afc34..0000000 --- a/HMCL/i18n.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include - -#include "i18n.h" - -#include "config.h" - -HLI18N HLI18N::Instance() { - HLI18N i18n = {.errorSelfPath = L"Failed to get the exe path.", - .errorInvalidHMCLJavaHome = - L"The Java path specified by HMCL_JAVA_HOME is invalid. Please update it to a valid Java " - "installation path or remove this environment variable.", - .errorJavaNotFound = - L"HMCL requires Java " HMCL_EXPECTED_JAVA_MAJOR_VERSION_STR " or later to run,\n" - L"Click 'OK' to start downloading java.\n" - L"Please restart HMCL after installing Java."}; - - const auto language = GetUserDefaultUILanguage(); - if (language == 2052) { // zh-CN - i18n.errorSelfPath = L"获取程序路径失败。"; - i18n.errorInvalidHMCLJavaHome = L"HMCL_JAVA_HOME 所指向的 Java 路径无效,请更新或删除该变量。\n"; - i18n.errorJavaNotFound = - L"HMCL 需要 Java " HMCL_EXPECTED_JAVA_MAJOR_VERSION_STR " 或更高版本才能运行,点击“确定”开始下载 Java。\n" - L"请在安装 Java 完成后重新启动 HMCL。"; - } - return i18n; -} diff --git a/HMCL/i18n.h b/HMCL/i18n.h deleted file mode 100644 index c1f5bec..0000000 --- a/HMCL/i18n.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -struct HLI18N { - static HLI18N Instance(); - - // Error Messages - LPCWSTR errorSelfPath; - LPCWSTR errorInvalidHMCLJavaHome; - LPCWSTR errorJavaNotFound; -}; diff --git a/HMCL/java.cpp b/HMCL/java.cpp deleted file mode 100644 index d82f210..0000000 --- a/HMCL/java.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include -#include -#include -#include - -#include "debug.h" -#include "java.h" - -HLJavaVersion HLJavaVersion::INVALID = HLJavaVersion{}; - -HLJavaVersion HLJavaVersion::FromJavaExecutable(const HLPath &filePath) { - UINT size = 0; - VS_FIXEDFILEINFO *pFileInfo; - DWORD dwSize = GetFileVersionInfoSizeW(filePath.path.c_str(), nullptr); - - if (!dwSize) return INVALID; - - std::vector data(dwSize); - if (!GetFileVersionInfoW(filePath.path.c_str(), 0, dwSize, &data[0])) { - return INVALID; - } - - if (!VerQueryValueW(&data[0], L"\\", (LPVOID *)&pFileInfo, &size)) { - return INVALID; - } - - return HLJavaVersion{.major = static_cast((pFileInfo->dwFileVersionMS >> 16) & 0xFFFF), - .minor = static_cast((pFileInfo->dwFileVersionMS >> 0) & 0xFFFF), - .build = static_cast((pFileInfo->dwFileVersionLS >> 16) & 0xFFFF), - .revision = static_cast((pFileInfo->dwFileVersionLS >> 0) & 0xFFFF)}; -} - -HLJavaVersion HLJavaVersion::FromString(const std::wstring &versionString) { - HLJavaVersion version = {}; - - uint16_t ver[4] = {0, 0, 0, 0}; // major, minor, build, revision - - int idx = 0; - - for (auto &ch : versionString) { - if (idx >= 4) { - break; - } - if (ch == L'.' || ch == L'_') { - if (idx == 0 && ver[0] == 1) { - // For legacy Java version 1.x - ver[0] = 0; - } else { - idx++; - } - } else if (ch >= L'0' && ch <= L'9') { - ver[idx] = ver[idx] * 10 + (ch - L'0'); - } - } - - return HLJavaVersion{ - .major = ver[0], - .minor = ver[1], - .build = ver[2], - .revision = ver[3], - }; -} - -bool HLLaunchJVM(const HLPath &javaExecutablePath, const HLJavaOptions &options, - const std::optional &version) { - std::wstring command; - command += '"'; - command += javaExecutablePath.path; - command += L'"'; - if (options.jvmOptions.has_value()) { - command += L' '; - command += options.jvmOptions.value(); - } else { - command += L" -Xmx1G -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=15"; - } - command += L" -jar \""; - command += options.jarPath; - command += L'"'; - - STARTUPINFOW startupInfo{.cb = sizeof(STARTUPINFOW)}; - PROCESS_INFORMATION processInformation{}; - - BOOL result = CreateProcessW(nullptr, &command[0], nullptr, nullptr, false, NORMAL_PRIORITY_CLASS, nullptr, - options.workdir.path.c_str(), &startupInfo, &processInformation); - if (result) { - HLDebugLog(L"Successfully launched HMCL with " + javaExecutablePath.path); - } else { - HLDebugLog(L"Failed to launch HMCL with " + javaExecutablePath.path); - } - - return result; -} - -void HLSearchJavaInDir(HLJavaList &result, const HLPath &basedir, LPCWSTR javaExecutableName) { - HLDebugLogVerbose(std::format(L"Searching in directory: {}", basedir.path)); - - HLPath pattern = basedir; - pattern /= L"*"; - - WIN32_FIND_DATA data; - HANDLE hFind = FindFirstFileW(pattern.path.c_str(), &data); // Search all subdirectory - if (hFind != INVALID_HANDLE_VALUE) { - do { - std::wstring fileName = data.cFileName; - if (fileName != L"." && fileName != L"..") { - result.TryAdd(basedir / data.cFileName / L"bin" / javaExecutableName); - } - } while (FindNextFile(hFind, &data)); - FindClose(hFind); - } -} - -static const LPCWSTR VENDORS[] = {L"Java", L"Microsoft", L"BellSoft", L"Zulu", L"Eclipse Foundation", - L"AdoptOpenJDK", L"Semeru"}; - -void HLSearchJavaInProgramFiles(HLJavaList &result, const HLPath &programFiles, LPCWSTR javaExecutableName) { - for (LPCWSTR vendorDir : VENDORS) { - HLPath dir = programFiles / vendorDir; - HLSearchJavaInDir(result, dir, javaExecutableName); - } -} - -void HLSearchJavaInRegistry(HLJavaList &result, LPCWSTR subKey, LPCWSTR javaExecutableName) { - HLDebugLogVerbose(std::format(L"Searching in registry key: HKEY_LOCAL_MACHINE\\{}", subKey)); - - constexpr int MAX_KEY_LENGTH = 255; - - WCHAR javaVer[MAX_KEY_LENGTH]; // buffer for subkey name, special for - // JavaVersion - WCHAR javaHome[MAX_PATH]; // buffer for JavaHome value - DWORD cbName; // size of name string - DWORD cSubKeys = 0; // number of subkeys - DWORD cbMaxSubKey; // longest subkey size - DWORD cValues; // number of values for key - DWORD cchMaxValue; // longest value name - DWORD cbMaxValueData; // longest value data - - HKEY hKey; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, KEY_WOW64_64KEY | KEY_READ, &hKey) != ERROR_SUCCESS) { - return; - } - - RegQueryInfoKeyW(hKey, nullptr, nullptr, nullptr, &cSubKeys, &cbMaxSubKey, nullptr, &cValues, &cchMaxValue, - &cbMaxValueData, nullptr, nullptr); - - if (!cSubKeys) { - RegCloseKey(hKey); - return; - } - - for (DWORD i = 0; i < cSubKeys; ++i) { - cbName = MAX_KEY_LENGTH; - if (RegEnumKeyExW(hKey, i, javaVer, &cbName, nullptr, nullptr, nullptr, nullptr) != ERROR_SUCCESS) { - continue; - } - - DWORD dataLength = sizeof(javaHome); - if (RegGetValueW(hKey, javaVer, L"JavaHome", RRF_RT_REG_SZ, nullptr, &javaHome[0], &dataLength) != ERROR_SUCCESS) { - continue; - } - - result.TryAdd(HLPath(javaHome) / L"bin" / javaExecutableName); - } - - RegCloseKey(hKey); -} - -void HLSearchJavaInPath(HLJavaList &result, const std::wstring &path, LPCWSTR javaExecutableName) { - std::size_t pos = 0; - while (pos < path.size()) { - auto end = path.find(L';', pos); - - if (end == std::wstring::npos) { - end = path.size(); - } - - // Skip leading spaces - while (pos < end && std::iswspace(path.at(pos))) { - pos++; - } - - // Skip trailing spaces - auto pathCount = end - pos; - while (pathCount > 0 && std::iswspace(path.at(pos + pathCount - 1))) { - pathCount--; - } - - if (pathCount > 0) { // Not empty - HLPath javaExecutable = path.substr(pos, pathCount); - javaExecutable /= javaExecutableName; - - // https://github.com/HMCL-dev/HMCL/issues/4079 - if (javaExecutable.path.find(L"\\Common Files\\Oracle\\Java\\") == std::wstring::npos) { - HLDebugLogVerbose(L"Checking " + javaExecutable.path); - result.TryAdd(javaExecutable); - } else { - HLDebugLogVerbose(std::format(L"Ignore Oracle Java {}", javaExecutable.path)); - } - } - pos = end + 1; - } -} - -bool HLJavaList::TryAdd(const HLPath &javaExecutable) { - if (!javaExecutable.IsRegularFile()) { - return false; - } - if (paths.contains(javaExecutable.path)) { - HLDebugLogVerbose(std::format(L"Ignore duplicate Java {}", javaExecutable.path)); - return false; - } - - auto version = HLJavaVersion::FromJavaExecutable(javaExecutable); - HLDebugLogVerbose(std::format(L"Found Java {}, Version {}", javaExecutable.path, version.ToWString(), - version.IsAcceptable() ? L"" : L", Ignored")); - if (!version.IsAcceptable()) { - return false; - } - - HLJavaRuntime javaRuntime = { - .version = version, - .executablePath = javaExecutable, - }; - - paths.insert(javaExecutable.path); - runtimes.push_back(javaRuntime); - return true; -} diff --git a/HMCL/java.h b/HMCL/java.h deleted file mode 100644 index 534c63b..0000000 --- a/HMCL/java.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "config.h" -#include "path.h" - -struct HLJavaVersion { - static HLJavaVersion INVALID; - - static HLJavaVersion FromJavaExecutable(const HLPath &filePath); - static HLJavaVersion FromString(const std::wstring &versionString); - - std::uint16_t major; - std::uint16_t minor; - std::uint16_t build; - std::uint16_t revision; - - std::strong_ordering operator<=>(const HLJavaVersion &other) const = default; - - [[nodiscard]] bool IsAcceptable() const { - return major >= HMCL_EXPECTED_JAVA_MAJOR_VERSION; - } - - [[nodiscard]] std::wstring ToWString() const { - if (major != 0) { - return std::format(L"{}.{}.{}.{}", major, minor, build, revision); - } else { - return L"Unknown"; - } - } -}; - -struct HLJavaRuntime { - HLJavaVersion version; - HLPath executablePath; - - std::strong_ordering operator<=>(const HLJavaRuntime &other) const { return version <=> other.version; } -}; - -struct HLJavaOptions { - HLPath workdir; - std::wstring jarPath; - std::optional jvmOptions; -}; - -struct HLJavaList { - std::vector runtimes; - std::unordered_set paths; - - bool TryAdd(const HLPath &javaExecutable); -}; - -bool HLLaunchJVM(const HLPath &javaExecutablePath, const HLJavaOptions &options, - const std::optional &version = std::nullopt); - -void HLSearchJavaInDir(HLJavaList &result, const HLPath &basedir, LPCWSTR javaExecutableName); - -void HLSearchJavaInProgramFiles(HLJavaList &result, const HLPath &programFiles, LPCWSTR javaExecutableName); - -void HLSearchJavaInRegistry(HLJavaList &result, LPCWSTR subKey, LPCWSTR javaExecutableName); - -void HLSearchJavaInPath(HLJavaList &result, const std::wstring &path, LPCWSTR javaExecutableName); \ No newline at end of file diff --git a/HMCL/main.cpp b/HMCL/main.cpp deleted file mode 100644 index b091652..0000000 --- a/HMCL/main.cpp +++ /dev/null @@ -1,220 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -#include "debug.h" -#include "i18n.h" -#include "path.h" -#include "platform.h" -#include "java.h" - -int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) { - HLVerboseOutput = HLGetEnvVar(L"HMCL_LAUNCHER_VERBOSE_OUTPUT").value_or(L"") != L"false"; - - LPCWSTR javaExecutableName; - if (HLAttachConsole()) { - javaExecutableName = L"java.exe"; - } else { - javaExecutableName = L"javaw.exe"; - } - - const auto arch = HLGetArchitecture(); - const bool isX64 = arch == HLArchitecture::X86_64; - const bool isARM64 = arch == HLArchitecture::ARM64; - const bool isX86 = arch == HLArchitecture::X86; - - const auto i18n = HLI18N::Instance(); - const auto selfPath = HLGetSelfPath(); - if (!selfPath.has_value()) { - HLDebugLog(L"Failed to get self path"); - MessageBoxW(nullptr, i18n.errorSelfPath, nullptr, MB_OK | MB_ICONERROR); - return EXIT_FAILURE; - } - - const HLJavaOptions options = {.workdir = selfPath.value().first, - .jarPath = selfPath.value().second, - .jvmOptions = HLGetEnvVar(L"HMCL_JAVA_OPTS")}; - HLDebugLog(std::format(L"*** HMCL Launcher {} ***", HMCL_LAUNCHER_VERSION)); - if (isX64) { - HLDebugLog(L"System Architecture: x86-64"); - } else if (isARM64) { - HLDebugLog(L"System Architecture: arm64"); - } else { - HLDebugLog(L"System Architecture: x86"); - } - - HLDebugLog(std::format(L"Working directory: {}", options.workdir.path)); - HLDebugLog(std::format(L"Exe File: {}\\{}", options.workdir.path, options.jarPath)); - if (options.jvmOptions.has_value()) { - HLDebugLog(std::format(L"JVM Options: {}", options.jvmOptions.value())); - } - - // If HMCL_JAVA_HOME is set, it should always be used - { - const auto hmclJavaHome = HLGetEnvPath(L"HMCL_JAVA_HOME"); - if (hmclJavaHome.has_value() && !hmclJavaHome.value().path.empty()) { - HLDebugLog(L"HMCL_JAVA_HOME: " + hmclJavaHome.value().path); - HLPath javaExecutablePath = hmclJavaHome.value() / L"bin" / javaExecutableName; - if (javaExecutablePath.IsRegularFile()) { - if (HLLaunchJVM(javaExecutablePath, options, std::nullopt)) { - return EXIT_SUCCESS; - } - } else { - HLDebugLog(std::format(L"Invalid HMCL_JAVA_HOME: {}", hmclJavaHome.value().path)); - } - MessageBoxW(nullptr, i18n.errorInvalidHMCLJavaHome, nullptr, MB_OK | MB_ICONERROR); - return EXIT_FAILURE; - } else { - HLDebugLogVerbose(L"HMCL_JAVA_HOME: Not Found"); - } - } - - // Try the Java packaged together. - { - HLPath javaExecutablePath = options.workdir; - if (isARM64) { - javaExecutablePath /= L"jre-arm64"; - } else if (isX64) { - javaExecutablePath /= L"jre-x64"; - } else { - javaExecutablePath /= L"jre-x86"; - } - javaExecutablePath /= L"bin"; - javaExecutablePath /= javaExecutableName; - if (javaExecutablePath.IsRegularFile()) { - HLDebugLog(std::format(L"Bundled JRE: {}", javaExecutablePath.path)); - if (HLLaunchJVM(javaExecutablePath, options, std::nullopt)) { - return EXIT_SUCCESS; - } - } else { - HLDebugLogVerbose(std::format(L"Bundled JRE: Not Found")); - } - } - - // ------ Search All Java ------ - - // To make the log look better, we first print JAVA_HOME - const auto javaHome = HLGetEnvPath(L"JAVA_HOME"); - if (javaHome.has_value() && !javaHome.value().path.empty()) { - HLDebugLog(L"JAVA_HOME: " + javaHome.value().path); - } else { - HLDebugLogVerbose(L"JAVA_HOME: Not Found"); - } - - HLJavaList javaRuntimes; - { - HLPath hmclJavaDir = options.workdir / L".hmcl\\java"; - if (isARM64) { - hmclJavaDir /= L"windows-arm64"; - } else if (isX64) { - hmclJavaDir /= L"windows-x86_64"; - } else { - hmclJavaDir /= L"windows-x86"; - } - HLSearchJavaInDir(javaRuntimes, hmclJavaDir, javaExecutableName); - } - - if (javaHome.has_value() && !javaHome.value().path.empty()) { - HLDebugLogVerbose(L"Checking JAVA_HOME"); - - HLPath javaExecutablePath = javaHome.value() / L"bin" / javaExecutableName; - if (javaExecutablePath.IsRegularFile()) { - javaRuntimes.TryAdd(javaExecutablePath); - } else { - HLDebugLog(std::format(L"JAVA_HOME is set to {}, but the Java executable {} does not exist", - javaHome.value().path, javaExecutablePath.path)); - } - } - - { - const auto appDataPath = HLGetEnvPath(L"APPDATA"); - if (appDataPath.has_value() && !appDataPath.value().path.empty()) { - HLPath hmclJavaDir = appDataPath.value() / L".hmcl\\java"; - if (isARM64) { - hmclJavaDir /= L"windows-arm64"; - } else if (isX64) { - hmclJavaDir /= L"windows-x86_64"; - } else { - hmclJavaDir /= L"windows-x86"; - } - HLSearchJavaInDir(javaRuntimes, hmclJavaDir, javaExecutableName); - } - } - - // Search Java in PATH - { - const auto paths = HLGetEnvVar(L"PATH"); - if (paths.has_value()) { - HLDebugLogVerbose(L"Searching in PATH"); - HLSearchJavaInPath(javaRuntimes, paths.value(), javaExecutableName); - } else { - HLDebugLog(L"PATH: Not Found"); - } - } - - // Search Java in C:\Program Files - { - std::optional programFilesPath; - if (isX64 || isARM64) { - programFilesPath = HLGetEnvPath(L"ProgramW6432"); - } else if (isX86) { - programFilesPath = HLGetEnvPath(L"ProgramFiles"); - } else { - programFilesPath = std::nullopt; - } - - if (programFilesPath.has_value() && !programFilesPath.value().path.empty()) { - HLSearchJavaInProgramFiles(javaRuntimes, programFilesPath.value(), javaExecutableName); - } else { - HLDebugLog(L"Failed to obtain the path to Program Files"); - } - } - - // Search Java in registry - HLSearchJavaInRegistry(javaRuntimes, L"SOFTWARE\\JavaSoft\\JDK", javaExecutableName); - HLSearchJavaInRegistry(javaRuntimes, L"SOFTWARE\\JavaSoft\\JRE", javaExecutableName); - - // Try to launch JVM - - if (javaRuntimes.runtimes.empty()) { - HLDebugLog(L"No Java runtime found."); - } else { - std::stable_sort(javaRuntimes.runtimes.begin(), javaRuntimes.runtimes.end()); - - if (HLVerboseOutput) { - std::wstring message = L"Found Java runtimes:"; - for (const auto &item : javaRuntimes.runtimes) { - message += L"\n - "; - message += item.executablePath.path; - message += L", Version "; - message += item.version.ToWString(); - } - - HLDebugLog(message); - } - - for (const auto &item : javaRuntimes.runtimes | std::views::reverse) { - if (HLLaunchJVM(item.executablePath, options, item.version)) { - return EXIT_SUCCESS; - } - } - } - - LPCWSTR downloadLink; - if (isARM64) { - downloadLink = L"https://docs.hmcl.net/downloads/windows/arm64.html"; - } else if (isX64) { - downloadLink = L"https://docs.hmcl.net/downloads/windows/x86_64.html"; - } else { - downloadLink = L"https://docs.hmcl.net/downloads/windows/x86.html"; - } - - if (MessageBoxW(nullptr, i18n.errorJavaNotFound, nullptr, MB_ICONWARNING | MB_OKCANCEL) == IDOK) { - ShellExecuteW(nullptr, nullptr, downloadLink, nullptr, nullptr, SW_SHOW); - } - return EXIT_FAILURE; -} \ No newline at end of file diff --git a/HMCL/path.cpp b/HMCL/path.cpp deleted file mode 100644 index d71dfde..0000000 --- a/HMCL/path.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include - -#include "path.h" - -void HLPath::AddBackslash() { - if (!path.empty() && path.back() != '\\' && path.back() != '/') { - path += '\\'; - } -} - -void HLPath::operator/=(const std::wstring& append) { - AddBackslash(); - path += append; -} - -HLPath HLPath::operator/(const std::wstring& append) const { - HLPath newPath = *this; - newPath /= append; - return newPath; -} - -bool HLPath::IsRegularFile() const { - DWORD attributes = GetFileAttributesW(path.c_str()); - - return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY) && - !(attributes & FILE_ATTRIBUTE_REPARSE_POINT); -} diff --git a/HMCL/path.h b/HMCL/path.h deleted file mode 100644 index ee74a75..0000000 --- a/HMCL/path.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -struct HLPath { - std::wstring path; - - HLPath() = default; - HLPath(const HLPath&) = default; - explicit(false) HLPath(std::wstring path) : path(std::move(path)) {} - explicit(false) HLPath(const wchar_t* path) : path(path) {} - - HLPath &operator=(const HLPath& other) = default; - - bool IsRegularFile() const; - - void AddBackslash(); - HLPath operator/(const std::wstring& append) const; - void operator/=(const std::wstring& append); -}; diff --git a/HMCL/platform.cpp b/HMCL/platform.cpp deleted file mode 100644 index ecbedd2..0000000 --- a/HMCL/platform.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include - -#include "path.h" -#include "platform.h" - -#ifndef PROCESSOR_ARCHITECTURE_ARM64 -#define PROCESSOR_ARCHITECTURE_ARM64 12 -#endif - -#ifndef IMAGE_FILE_MACHINE_ARM64 -#define IMAGE_FILE_MACHINE_ARM64 0xAA64 -#endif - -extern "C" { -__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; -__declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 0x00000001; -} - -HLArchitecture HLGetArchitecture() { - // https://learn.microsoft.com/windows/win32/api/wow64apiset/nf-wow64apiset-iswow64process2 - auto fnIsWow64Process2 = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"Kernel32.dll"), "IsWow64Process2")); - - if (fnIsWow64Process2 != nullptr) { - USHORT uProcessMachine = 0; - USHORT uNativeMachine = 0; - if (fnIsWow64Process2(GetCurrentProcess(), &uProcessMachine, &uNativeMachine)) { - if (uNativeMachine == IMAGE_FILE_MACHINE_ARM64) { - return HLArchitecture::ARM64; - } - - if (uNativeMachine == IMAGE_FILE_MACHINE_AMD64) { - return HLArchitecture::X86_64; - } - - return HLArchitecture::X86; - } - } - - SYSTEM_INFO systemInfo; - GetNativeSystemInfo(&systemInfo); - - if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM64) { - return HLArchitecture::ARM64; - } - - if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) { - return HLArchitecture::X86_64; - } - - return HLArchitecture::X86; -} - -std::optional> HLGetSelfPath() { - DWORD res, size = MAX_PATH; - std::wstring selfPath(size, L'\0'); - while ((res = GetModuleFileNameW(nullptr, &selfPath[0], size)) == size) { - selfPath.resize(size += MAX_PATH); - } - if (res == 0) return std::nullopt; - - selfPath.resize(size - MAX_PATH + res); - - size_t last_slash = selfPath.find_last_of(L"/\\"); - if (last_slash != std::wstring::npos && last_slash + 1 < selfPath.size()) { - return std::optional{std::pair{HLPath{selfPath.substr(0, last_slash)}, selfPath.substr(last_slash + 1)}}; - } else { - return std::nullopt; - } -} - -std::optional HLGetEnvVar(LPCWSTR name) { - DWORD size = MAX_PATH; - std::wstring out(size, L'\0'); - - while (size < 32 * 1024) { - SetLastError(ERROR_SUCCESS); - DWORD res = GetEnvironmentVariableW(name, &out[0], size); - if (res == 0 && GetLastError() != ERROR_SUCCESS) { - return std::nullopt; - } - - if (res < size) { - out.resize(res); - return std::optional{out}; - } - - if (res == size) { - // I think it's not possible, but I'm not really sure, so do something to avoid an infinite loop. - size = res + 1; - } else { - size = res; - } - out.resize(size); - } - - return std::nullopt; -} - -std::optional HLGetEnvPath(LPCWSTR name) { - auto res = HLGetEnvVar(name); - if (res.has_value()) { - return HLPath{res.value()}; - } else { - return std::nullopt; - } -} diff --git a/HMCL/platform.h b/HMCL/platform.h deleted file mode 100644 index 62f60f1..0000000 --- a/HMCL/platform.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "path.h" - -enum HLArchitecture { X86, X86_64, ARM64 }; - -HLArchitecture HLGetArchitecture(); - -std::optional> HLGetSelfPath(); - -std::optional HLGetEnvVar(LPCWSTR name); - -std::optional HLGetEnvPath(LPCWSTR name); \ No newline at end of file diff --git a/HMCL/res.rc b/HMCL/res.rc new file mode 100644 index 0000000..e2528b6 --- /dev/null +++ b/HMCL/res.rc @@ -0,0 +1,35 @@ +#include + +#define HMCL_LAUNCHER_VERSION "3.7.0.1" +#define HMCL_EXPECTED_JAVA_MAJOR_VERSION 17 + +IDI_HMCL ICON "resources/HMCL.ico" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 3,7,0,1 +PRODUCTVERSION 3,7,0,1 +FILEFLAGSMASK 0x3fL +FILEFLAGS 0x0L +FILEOS 0x40004L +FILETYPE 0x1L +FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "080404b0" + BEGIN + VALUE "CompanyName", "huanghongxun" + VALUE "FileDescription", "Hello Minecraft! Launcher For Windows" + VALUE "FileVersion", HMCL_LAUNCHER_VERSION + VALUE "InternalName", "HMCL.exe" + VALUE "LegalCopyright", "Copyright (C) 2025 huangyuhui" + VALUE "OriginalFilename", "HMCL.exe" + VALUE "ProductName", "Hello Minecraft! Launcher" + VALUE "ProductVersion", HMCL_LAUNCHER_VERSION + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x804, 1200 + END +END \ No newline at end of file diff --git a/HMCL.ico b/HMCL/resources/HMCL.ico similarity index 100% rename from HMCL.ico rename to HMCL/resources/HMCL.ico diff --git a/HMCL/src/debug.rs b/HMCL/src/debug.rs new file mode 100644 index 0000000..f76e080 --- /dev/null +++ b/HMCL/src/debug.rs @@ -0,0 +1,80 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Write}; +use std::sync::{Mutex, OnceLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub struct DebugLogger { + file: Mutex>, + verbose: bool, +} + +static GLOBAL_LOGGER: OnceLock = OnceLock::new(); + +impl DebugLogger { + pub fn init() -> &'static DebugLogger { + GLOBAL_LOGGER.get_or_init(|| { + let verbose = std::env::var_os("HMCL_LAUNCHER_VERBOSE_OUTPUT") + .map(|v| v != "false") + .unwrap_or(false); + + let logger = DebugLogger { + file: Mutex::new(None), + verbose, + }; + + if let Ok(mut path) = std::env::current_dir() { + path.push("logs"); + let _ = std::fs::create_dir_all(&path); + + for i in 0..9 { + let mut log_path = path.join("hmclauncher.log"); + if i > 0 { + log_path = path.join(format!("hmclauncher.log.{}", i)); + } + + if let Ok(file) = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&log_path) + { + *logger.file.lock().unwrap() = Some(file); + break; + } + } + } + + logger + }) + } + + pub fn global() -> &'static DebugLogger { + GLOBAL_LOGGER.get().expect("DebugLogger not initialized") + } + + pub fn log(&self, message: String) { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let hours = (timestamp / 3600) % 24; + let minutes = (timestamp / 60) % 60; + let seconds = timestamp % 60; + + let formatted = format!("[{:02}:{:02}:{:02}] [HMCLauncher] {}", + hours, minutes, seconds, message); + + println!("{}", formatted); + + if let Some(ref mut file) = *self.file.lock().unwrap() { + let _ = writeln!(file, "{}", formatted); + } + } + + pub fn log_verbose(&self, message: String) { + if self.verbose { + self.log(message); + } + } +} \ No newline at end of file diff --git a/HMCL/src/i18n.rs b/HMCL/src/i18n.rs new file mode 100644 index 0000000..b323667 --- /dev/null +++ b/HMCL/src/i18n.rs @@ -0,0 +1,32 @@ +use once_cell::sync::Lazy; + +pub struct I18n { + pub error_self_path: &'static str, + pub error_invalid_hmcl_java_home: &'static str, + pub error_java_notfound: &'static str, +} +use windows::Win32::Globalization::GetUserDefaultUILanguage; + +impl I18n { + pub fn load() -> &'static I18n { + static I18N: Lazy = Lazy::new(|| { + let language = unsafe { GetUserDefaultUILanguage() }; + + if language == 2052 { // zh-CN + I18n { + error_self_path: "获取程序路径失败。", + error_invalid_hmcl_java_home: "HMCL_JAVA_HOME 所指向的 Java 路径无效,请更新或删除该变量。\n", + error_java_notfound: "HMCL 需要 Java 17 或更高版本才能运行,点击“确定”开始下载 Java。\n请在安装 Java 完成后重新启动 HMCL。", + } + } else { + I18n { + error_self_path: "Failed to get the exe path.", + error_invalid_hmcl_java_home: "The Java path specified by HMCL_JAVA_HOME is invalid. Please update it to a valid Java installation path or remove this environment variable.", + error_java_notfound: "HMCL requires Java 17 or later to run,\nClick 'OK' to start downloading java.\nPlease restart HMCL after installing Java.", + } + } + }); + + &I18N + } +} \ No newline at end of file diff --git a/HMCL/src/java/finder.rs b/HMCL/src/java/finder.rs new file mode 100644 index 0000000..b8395ed --- /dev/null +++ b/HMCL/src/java/finder.rs @@ -0,0 +1,307 @@ +use std::path::{Path, PathBuf}; +use std::collections::HashSet; +use windows::core::PWSTR; +use windows::Win32::System::Registry::{HKEY, RegOpenKeyExW, RegCloseKey, RegQueryInfoKeyW, RegEnumKeyExW, RegGetValueW, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY, RRF_RT_REG_SZ}; +use windows::core::PCWSTR; +use std::ffi::OsString; +use std::os::windows::ffi::OsStrExt; + +use super::version::JavaVersion; +use crate::debug::DebugLogger; +use crate::java::platform::{Architecture, Platform}; + +#[derive(Clone)] +pub struct JavaRuntime { + pub version: JavaVersion, + pub executable: PathBuf, +} + +pub struct JavaFinder { + expected_major_version: u16, + found_java: Vec, + seen_paths: HashSet, +} + +impl JavaFinder { + pub fn new(expected_major_version: u16) -> Self { + Self { + expected_major_version, + found_java: Vec::new(), + seen_paths: HashSet::new(), + } + } + + pub fn find_java_installations(&mut self, java_exe_name: &str) { + let debug_logger = DebugLogger::global(); + let platform = Platform::current(); + + // Search java in JAVA_HOME + if let Ok(java_home) = std::env::var("JAVA_HOME") { + debug_logger.log_verbose(format!("Checking JAVA_HOME: {}", java_home)); + let java_executable = Path::new(&java_home).join("bin").join(java_exe_name); + self.try_add_java(java_executable); + } + + if let Ok(path_env) = std::env::var("PATH") { + debug_logger.log_verbose("Searching in PATH".to_string()); + for path_dir in path_env.split(';') { + if !path_dir.trim().is_empty() { + let java_executable = Path::new(path_dir.trim()).join(java_exe_name); + self.try_add_java(java_executable); + } + } + } + + if let Ok(program_files) = std::env::var("ProgramFiles") { + let program_files_path = Path::new(&program_files); + self.search_in_program_files(program_files_path, java_exe_name); + } + + self.search_in_registry("SOFTWARE\\JavaSoft\\JDK", java_exe_name); + self.search_in_registry("SOFTWARE\\JavaSoft\\JRE", java_exe_name); + + if let Ok(current_dir) = std::env::current_dir() { + let hmcl_java_dir = current_dir.join(".hmcl").join("java").join( + match platform.architecture { + Architecture::Arm64 => "windows-arm64", + Architecture::X86_64 => "windows-x86_64", + Architecture::X86 => "windows-x86", + } + ); + + if hmcl_java_dir.exists() { + self.search_in_directory(&hmcl_java_dir, java_exe_name); + } + } + + if let Ok(appdata) = std::env::var("APPDATA") { + let hmcl_java_dir = Path::new(&appdata).join(".hmcl").join("java").join( + match platform.architecture { + Architecture::Arm64 => "windows-arm64", + Architecture::X86_64 => "windows-x86_64", + Architecture::X86 => "windows-x86", + } + ); + + if hmcl_java_dir.exists() { + self.search_in_directory(&hmcl_java_dir, java_exe_name); + } + } + } + + fn try_add_java(&mut self, executable: PathBuf) { + let debug_logger = DebugLogger::global(); + + if !executable.exists() { + return; + } + + let path_str = executable.to_string_lossy().to_string(); + if self.seen_paths.contains(&path_str) { + debug_logger.log_verbose(format!("Ignore duplicate Java: {}", path_str)); + return; + } + + if let Some(version) = JavaVersion::from_java_executable(executable.as_ref()) { + if !version.is_acceptable(self.expected_major_version) { + debug_logger.log_verbose(format!("Java {} version {} not acceptable", + path_str, version)); + return; + } + + self.seen_paths.insert(path_str.clone()); + self.found_java.push(JavaRuntime { + version, + executable, + }); + + debug_logger.log_verbose(format!("Found Java {} version {}", + path_str, self.found_java.last().unwrap().version)); + } + } + + fn search_in_directory(&mut self, dir: &Path, java_exe_name: &str) { + let debug_logger = DebugLogger::global(); + debug_logger.log_verbose(format!("Searching in directory: {}", dir.display())); + + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.filter_map(Result::ok) { + if let Ok(file_type) = entry.file_type() { + if file_type.is_dir() { + let java_executable = entry.path().join("bin").join(java_exe_name); + self.try_add_java(java_executable); + } + } + } + } + } + + fn search_in_program_files(&mut self, program_files: &Path, java_exe_name: &str) { + let vendors = ["Java", "Microsoft", "BellSoft", "Zulu", + "Eclipse Foundation", "AdoptOpenJDK", "Semeru"]; + + for vendor in vendors { + let vendor_dir = program_files.join(vendor); + if vendor_dir.exists() { + self.search_in_directory(&vendor_dir, java_exe_name); + } + } + } + + fn search_in_registry(&mut self, subkey: &str, java_exe_name: &str) { + let debug_logger = DebugLogger::global(); + debug_logger.log_verbose(format!("Searching in registry: {}", subkey)); + + let subkey_wide: Vec = OsString::from(subkey) + .encode_wide() + .chain(Some(0)) + .collect(); + + let mut hkey: HKEY = HKEY::default(); + + unsafe { + if RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + PCWSTR(subkey_wide.as_ptr()), + Some(0), + KEY_READ | KEY_WOW64_64KEY, + &mut hkey, + ) != windows::Win32::Foundation::WIN32_ERROR(0) { + return; + } + } + + let mut num_subkeys = 0u32; + let mut max_subkey_len = 0u32; + + unsafe { + if RegQueryInfoKeyW( + hkey, + Some(PWSTR::null()), + None, + None, + Some(&mut num_subkeys), + Some(&mut max_subkey_len), + None, + None, + None, + None, + None, + None, + ) != windows::Win32::Foundation::WIN32_ERROR(0) { + let _ = RegCloseKey(hkey); + return; + } + } + + let mut buffer = vec![0u16; (max_subkey_len + 1) as usize]; + + for i in 0..num_subkeys { + let mut buffer_len = buffer.len() as u32; + + unsafe { + if RegEnumKeyExW( + hkey, + i, + Some(PWSTR(buffer.as_mut_ptr())), + &mut buffer_len, + None, + Some(PWSTR::null()), + None, + None, + ) != windows::Win32::Foundation::WIN32_ERROR(0) { + continue; + } + } + + let java_home_path = buffer[..buffer_len as usize] + .iter() + .map(|&c| c as u16) + .collect::>(); + + let mut java_home = vec![0u16; 1024]; + let mut java_home_size = (java_home.len() * 2) as u32; + + unsafe { + if RegGetValueW( + hkey, + PCWSTR(java_home_path.as_ptr()), + PCWSTR("JavaHome\0".encode_utf16().collect::>().as_ptr()), + RRF_RT_REG_SZ, + None, + Some(java_home.as_mut_ptr() as *mut _), + Some(&mut java_home_size), + ) == windows::Win32::Foundation::WIN32_ERROR(0) { + let java_home_str = String::from_utf16_lossy( + &java_home[..(java_home_size as usize / 2) - 1] + ); + + let java_executable = Path::new(&java_home_str) + .join("bin") + .join(java_exe_name); + + self.try_add_java(java_executable); + } + } + } + + unsafe { + let _ = RegCloseKey(hkey); + } + } + + pub fn best_java(&self) -> Option<&JavaRuntime> { + self.found_java.iter().max_by_key(|r| &r.version) + } +} + +pub struct JavaLauncher { + java_executable: PathBuf, + workdir: PathBuf, + jar_path: Option, + jvm_options: Option, +} + +impl JavaLauncher { + pub fn new( + java_executable: PathBuf, + workdir: PathBuf, + jar_path: Option, + jvm_options: Option, + ) -> Self { + Self { + java_executable, + workdir, + jar_path, + jvm_options, + } + } + + pub fn launch(&self) -> std::io::Result<()> { + use std::process::Command; + + let debug_logger = DebugLogger::global(); + + let mut command = Command::new(&self.java_executable); + command.current_dir(&self.workdir); + + if let Some(options) = &self.jvm_options { + command.args(options.split_whitespace()); + } else { + command.args(&["-Xmx1G", "-XX:MinHeapFreeRatio=5", "-XX:MaxHeapFreeRatio=15"]); + } + + if let Some(jar_path) = &self.jar_path { + command.arg("-jar").arg(jar_path); + } + + debug_logger.log(format!("Launching Java: {:?}", command)); + + let mut child = command.spawn()?; + + // 等待进程结束 + let _ = child.wait(); + + Ok(()) + } +} \ No newline at end of file diff --git a/HMCL/src/java/mod.rs b/HMCL/src/java/mod.rs new file mode 100644 index 0000000..689a098 --- /dev/null +++ b/HMCL/src/java/mod.rs @@ -0,0 +1,4 @@ +mod version; +mod finder; +pub(crate) mod platform; +pub use finder::{JavaFinder, JavaLauncher}; \ No newline at end of file diff --git a/HMCL/src/java/platform.rs b/HMCL/src/java/platform.rs new file mode 100644 index 0000000..57f147f --- /dev/null +++ b/HMCL/src/java/platform.rs @@ -0,0 +1,39 @@ +use windows::Win32::System::SystemInformation::{ + GetNativeSystemInfo, SYSTEM_INFO, PROCESSOR_ARCHITECTURE_ARM64, + PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_INTEL, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Architecture { + X86, + X86_64, + Arm64, +} + +pub struct Platform { + pub architecture: Architecture, +} + +impl Platform { + pub fn current() -> Self { + let architecture = Self::detect_architecture(); + + Self { + architecture, + } + } + + fn detect_architecture() -> Architecture { + let mut system_info = SYSTEM_INFO::default(); + unsafe { GetNativeSystemInfo(&mut system_info) }; + + unsafe { + match system_info.Anonymous.Anonymous.wProcessorArchitecture { + PROCESSOR_ARCHITECTURE_ARM64 => Architecture::Arm64, + PROCESSOR_ARCHITECTURE_AMD64 => Architecture::X86_64, + PROCESSOR_ARCHITECTURE_INTEL => Architecture::X86, + _ => Architecture::X86, + } + } + } +} \ No newline at end of file diff --git a/HMCL/src/java/version.rs b/HMCL/src/java/version.rs new file mode 100644 index 0000000..5745653 --- /dev/null +++ b/HMCL/src/java/version.rs @@ -0,0 +1,88 @@ +use std::fmt; +use std::os::windows::ffi::OsStrExt; +use windows::core::w; +use windows::Win32::Storage::FileSystem::VerQueryValueW; +use windows::Win32::Storage::FileSystem::VS_FIXEDFILEINFO; +use windows::Win32::Storage::FileSystem::GetFileVersionInfoW; +use windows::Win32::Storage::FileSystem::GetFileVersionInfoSizeW; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct JavaVersion { + pub major: u16, + pub minor: u16, + pub build: u16, + pub revision: u16, +} + +impl JavaVersion { + pub fn from_java_executable(path: &std::path::Path) -> Option { + use windows::core::PCWSTR; + + let path_wide: Vec = path + .as_os_str() + .encode_wide() + .chain(Some(0)) + .collect(); + + let size = unsafe { + GetFileVersionInfoSizeW(PCWSTR(path_wide.as_ptr()), None) + }; + + if size == 0 { + return None; + } + + let mut buffer = vec![0u8; size as usize]; + + unsafe { + if GetFileVersionInfoW( + PCWSTR(path_wide.as_ptr()), + Some(0), + size, + buffer.as_mut_ptr() as _, + ).is_err() { + return None; + } + } + + let mut file_info: *const VS_FIXEDFILEINFO = std::ptr::null(); + let mut file_info_size = 0u32; + + unsafe { + if !VerQueryValueW( + buffer.as_ptr() as _, + PCWSTR::from_raw(w!("\\\0").as_ptr()), + &mut file_info as *mut _ as _, + &mut file_info_size, + ).as_bool() { + return None; + } + } + + if file_info_size == 0 || file_info.is_null() { + return None; + } + + let file_info = unsafe { *file_info }; + + Some(JavaVersion { + major: ((file_info.dwFileVersionMS >> 16) & 0xFFFF) as u16, + minor: (file_info.dwFileVersionMS & 0xFFFF) as u16, + build: ((file_info.dwFileVersionLS >> 16) & 0xFFFF) as u16, + revision: (file_info.dwFileVersionLS & 0xFFFF) as u16, + }) + } + pub fn is_acceptable(&self, expected_major: u16) -> bool { + self.major >= expected_major + } +} + +impl fmt::Display for JavaVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.major == 0 { + write!(f, "Unknown") + } else { + write!(f, "{}.{}.{}.{}", self.major, self.minor, self.build, self.revision) + } + } +} \ No newline at end of file diff --git a/HMCL/src/main.rs b/HMCL/src/main.rs new file mode 100644 index 0000000..21bccf5 --- /dev/null +++ b/HMCL/src/main.rs @@ -0,0 +1,201 @@ +#![windows_subsystem = "windows"] +mod debug; +mod i18n; +mod java; + +use windows::{ + Win32::UI::WindowsAndMessaging::{ + MessageBoxW, IDOK, MB_ICONERROR, MB_ICONWARNING, MB_OK, MB_OKCANCEL, + }, + Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS}, +}; + +use std::env; +use std::process; +use std::ffi::OsString; +use anyhow::{Context, Result}; +use std::path::{Path, PathBuf}; + +use debug::DebugLogger; +use i18n::I18n; +use java::{JavaFinder, JavaLauncher}; +use java::platform::{Architecture, Platform}; + +fn main() -> Result<()> { + let debug_logger = DebugLogger::init(); + + // Check if it needs to show detailed information + env::var_os("HMCL_LAUNCHER_VERBOSE_OUTPUT") + .map(|v| v != "false") + .unwrap_or(false); + + let _ = unsafe { AttachConsole(ATTACH_PARENT_PROCESS) }; + + let i18n = I18n::load(); + let platform = Platform::current(); + + debug_logger.log(format!("*** HMCL Launcher {} ***", env!("CARGO_PKG_VERSION"))); + debug_logger.log(format!("System Architecture: {:?}", platform.architecture)); + + let self_exe_path = env::current_exe() + .context(i18n.error_self_path)?; + debug_logger.log(format!("Working directory: {}", self_exe_path.parent().unwrap_or(&self_exe_path).display())); + debug_logger.log(format!("Exe File: {}", self_exe_path.display())); + + let java_exe_name = if unsafe { AttachConsole(ATTACH_PARENT_PROCESS) }.is_ok() { + "java.exe" + } else { + "javaw.exe" + }; + + // If HMCL_JAVA_HOME is set, it should always be used + if let Some(hmcl_java_home) = env::var_os("HMCL_JAVA_HOME") { + debug_logger.log(format!("HMCL_JAVA_HOME: {}", hmcl_java_home.to_string_lossy())); + + let java_executable = Path::new(&hmcl_java_home).join("bin").join(java_exe_name); + if java_executable.is_file() { + let launcher = JavaLauncher::new( + java_executable.clone(), + self_exe_path.parent().unwrap_or(&self_exe_path).to_path_buf(), + self_exe_path.file_name().map(|s| PathBuf::from(s)), + env::var("HMCL_JAVA_OPTS").ok(), + ); + + if launcher.launch().is_ok() { + return Ok(()); + } + } + + // HMCL_JAVA_HOME is set but not valid + show_message_box( + &i18n.error_invalid_hmcl_java_home, + None, + (MB_ICONERROR | MB_OK).0, + ); + process::exit(1); + } + + if let Err(e) = find_and_launch_java(&platform, &self_exe_path, java_exe_name, &i18n) { + debug_logger.log(format!("Failed to launch Java: {}", e)); + + let download_link = match platform.architecture { + Architecture::Arm64 => "https://docs.hmcl.net/downloads/windows/arm64.html", + Architecture::X86_64 => "https://docs.hmcl.net/downloads/windows/x86_64.html", + Architecture::X86 => "https://docs.hmcl.net/downloads/windows/x86.html", + }; + + if show_message_box( + &i18n.error_java_notfound, + None, + (MB_ICONWARNING | MB_OKCANCEL).0, + ) == IDOK.0 { + let _ = open_url(download_link); + } + + process::exit(1); + } + + Ok(()) +} + +fn find_and_launch_java( + platform: &Platform, + self_exe_path: &Path, + java_exe_name: &str, + _i18n: &I18n, +) -> Result<()> { + const EXPECTED_JAVA_MAJOR_VERSION: u16 = 17; + + let debug_logger = DebugLogger::global(); + + let mut finder = JavaFinder::new(EXPECTED_JAVA_MAJOR_VERSION); + + finder.find_java_installations(java_exe_name); + + let workdir = self_exe_path.parent().unwrap_or(self_exe_path); + let bundled_jre_path = get_bundled_jre_path(platform, workdir, java_exe_name); + + if bundled_jre_path.is_file() { + debug_logger.log(format!("Bundled JRE: {}", bundled_jre_path.display())); + + let launcher = JavaLauncher::new( + bundled_jre_path, + workdir.to_path_buf(), + self_exe_path.file_name().map(|s| PathBuf::from(s)), + env::var("HMCL_JAVA_OPTS").ok(), + ); + + return Ok(launcher.launch()?); + } + + debug_logger.log("Bundled JRE: Not Found".to_string()); + + + if let Some(best_java) = finder.best_java() { + debug_logger.log(format!("Using Java: {} (Version {})", + best_java.executable.display(), + best_java.version)); + + let launcher = JavaLauncher::new( + best_java.executable.clone(), + workdir.to_path_buf(), + self_exe_path.file_name().map(|s| PathBuf::from(s)), + env::var("HMCL_JAVA_OPTS").ok(), + ); + + return Ok(launcher.launch()?); + } + + anyhow::bail!("No suitable Java installation found"); +} + +fn get_bundled_jre_path(platform: &Platform, workdir: &Path, java_exe_name: &str) -> PathBuf { + let jre_dir = match platform.architecture { + Architecture::Arm64 => "jre-arm64", + Architecture::X86_64 => "jre-x64", + Architecture::X86 => "jre-x86", + }; + + workdir.join(jre_dir).join("bin").join(java_exe_name) +} + +fn show_message_box(text: &str, caption: Option<&str>, flags: u32) -> i32 { + use std::os::windows::ffi::OsStrExt; + use windows::core::PCWSTR; + + let text_wide: Vec = OsString::from(text).encode_wide().chain(Some(0)).collect(); + let caption_wide: Vec = caption + .map(|c| OsString::from(c).encode_wide().chain(Some(0)).collect()) + .unwrap_or_else(|| vec![0]); + + unsafe { + MessageBoxW( + None, + PCWSTR(text_wide.as_ptr()), + PCWSTR(caption_wide.as_ptr()), + windows::Win32::UI::WindowsAndMessaging::MESSAGEBOX_STYLE(flags), + ).0 as i32 + } +} + +fn open_url(url: &str) -> Result<()> { + use windows::Win32::UI::Shell::ShellExecuteW; + use windows::core::PCWSTR; + use std::os::windows::ffi::OsStrExt; + + let url_wide: Vec = OsString::from(url).encode_wide().chain(Some(0)).collect(); + let operation_wide: Vec = OsString::from("open").encode_wide().chain(Some(0)).collect(); + + unsafe { + ShellExecuteW( + None, + PCWSTR(operation_wide.as_ptr()), + PCWSTR(url_wide.as_ptr()), + None, + None, + windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD(5), // SW_SHOW + ); + } + + Ok(()) +} \ No newline at end of file diff --git a/README.md b/README.md index fda32c7..61f9437 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,13 @@ HMCL Launcher for Windows. -## Build - -```powershell -cmake -A Win32 -B build && cmake --build build --config Release +## Setup +1. Clone this repository. +2. Run `cargo run` in your terminal. +```bash +git clone https://github.com/HMCL-dev/HMCLauncher.git +cd HMCLauncher +cargo run ``` ## License