243 行
9.0 KiB
C++
243 行
9.0 KiB
C++
//
|
|
// main.cpp — Win32 entry point and window orchestration.
|
|
//
|
|
// Two modes:
|
|
//
|
|
// GUI (no arguments): splash plays its ~1 s intro, the main window fades
|
|
// in, and clicking a row's Inject opens the progress overlay which runs
|
|
// the injection on a worker thread; on completion the loader fades out.
|
|
//
|
|
// Headless (command line): inject without any UI and exit. Progress is
|
|
// printed to the parent console when present (exit code 0 = bootstrap
|
|
// complete); failures pop a message box. Useful for launcher integrations
|
|
// and scripted starts:
|
|
// OpenZenLoader.exe 34028 --nogui inject into PID 34028, no UI
|
|
// OpenZenLoader.exe 34028 same (--nogui implied by the pid)
|
|
// OpenZenLoader.exe --nogui inject into every detected
|
|
// Minecraft instance
|
|
// OpenZenLoader.exe --help print usage, exit 0
|
|
//
|
|
// The GUI itself is pure Win32 + GDI+ (no Qt): the whole UI lives in wgfx /
|
|
// splash / main / overlay and links only system libraries.
|
|
//
|
|
|
|
#include "loader.h"
|
|
#include "main_win.h"
|
|
#include "overlay_win.h"
|
|
#include "splash_win.h"
|
|
#include "wgfx.h"
|
|
|
|
#include <cstdlib>
|
|
#include <gdiplus.h>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
const wchar_t* kUsage =
|
|
L"OpenZenLoader - command line mode\n"
|
|
L"\n"
|
|
L"Usage:\n"
|
|
L" OpenZenLoader.exe <JavaPID> [--nogui]\n"
|
|
L" Inject OpenZen into the running Java process (must be a\n"
|
|
L" javaw.exe/java.exe process). Prints progress; exit code 0 = ok.\n"
|
|
L" OpenZenLoader.exe --nogui\n"
|
|
L" Inject into every detected Minecraft instance.\n"
|
|
L" OpenZenLoader.exe --help\n"
|
|
L" Show this help (exit code 0).\n"
|
|
L"\n"
|
|
L"Exit codes: 0 = success, 1 = injection failed, 2 = bad arguments\n";
|
|
|
|
// A /SUBSYSTEM:WINDOWS binary has no console of its own. When launched from a
|
|
// terminal we attach to the parent's console so WriteConsoleW output lands
|
|
// back in the caller's window. Best-effort: when the parent has no console
|
|
// (double-click launch) output is simply dropped and the exit code carries
|
|
// the result.
|
|
bool AttachParentConsole() {
|
|
if (!AttachConsole(ATTACH_PARENT_PROCESS)) return false;
|
|
HANDLE conout = CreateFileW(L"CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE,
|
|
nullptr, OPEN_EXISTING, 0, nullptr);
|
|
if (conout == INVALID_HANDLE_VALUE) return false;
|
|
SetStdHandle(STD_OUTPUT_HANDLE, conout);
|
|
SetStdHandle(STD_ERROR_HANDLE, conout);
|
|
return true;
|
|
}
|
|
|
|
void Write(const std::wstring& text) {
|
|
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
if (!out || out == INVALID_HANDLE_VALUE) return;
|
|
DWORD written = 0;
|
|
WriteConsoleW(out, text.c_str(), static_cast<DWORD>(text.size()),
|
|
&written, nullptr);
|
|
}
|
|
|
|
// Headless: inject one pid, report via exit code (+ message box on error).
|
|
int RunHeadless(unsigned long pid) {
|
|
Write(L"Injecting into PID " + std::to_wstring(pid) + L"...\n");
|
|
std::wstring err = loader::inject(pid);
|
|
if (err.empty()) {
|
|
Write(L"Injection complete: OpenZen bootstrap finished; "
|
|
L"the client should appear on the next tick.\n");
|
|
ui::LogLine(L"headless inject ok, pid=" + std::to_wstring(pid));
|
|
return 0;
|
|
}
|
|
Write(L"Injection failed: " + err + L"\n");
|
|
ui::LogLine(L"headless inject FAILED, pid=" + std::to_wstring(pid) +
|
|
L": " + err);
|
|
MessageBoxW(nullptr,
|
|
(L"Injection failed (PID " + std::to_wstring(pid) + L"):\n" +
|
|
err)
|
|
.c_str(),
|
|
L"OpenZen Loader", MB_OK | MB_ICONERROR);
|
|
return 1;
|
|
}
|
|
|
|
// Headless: inject into every Minecraft instance we can find.
|
|
int RunHeadlessAll() {
|
|
auto procs = loader::list_java_processes();
|
|
std::vector<unsigned long> targets;
|
|
std::vector<std::wstring> labels;
|
|
for (const auto& jp : procs) {
|
|
if (!ui::LooksLikeMinecraft(jp)) continue;
|
|
targets.push_back(jp.pid);
|
|
labels.push_back(jp.window_title.empty() ? jp.window_class
|
|
: jp.window_title);
|
|
}
|
|
if (targets.empty()) {
|
|
ui::LogLine(L"headless inject: no Minecraft instance detected");
|
|
Write(L"No Minecraft instances detected.\n");
|
|
MessageBoxW(nullptr, L"No Minecraft instances detected.",
|
|
L"OpenZen Loader", MB_OK | MB_ICONWARNING);
|
|
return 1;
|
|
}
|
|
|
|
int failed = 0;
|
|
for (size_t i = 0; i < targets.size(); ++i) {
|
|
std::wstring err = loader::inject(targets[i]);
|
|
if (err.empty()) {
|
|
Write(L"Injected " + labels[i] + L" (PID " +
|
|
std::to_wstring(targets[i]) + L"): bootstrap complete.\n");
|
|
ui::LogLine(L"headless inject ok, pid=" +
|
|
std::to_wstring(targets[i]));
|
|
} else {
|
|
++failed;
|
|
Write(L"Inject FAILED for " + labels[i] + L" (PID " +
|
|
std::to_wstring(targets[i]) + L"): " + err + L"\n");
|
|
ui::LogLine(L"headless inject FAILED, pid=" +
|
|
std::to_wstring(targets[i]) + L": " + err);
|
|
MessageBoxW(nullptr,
|
|
(L"Injection failed (" + labels[i] + L", PID " +
|
|
std::to_wstring(targets[i]) + L"):\n" + err)
|
|
.c_str(),
|
|
L"OpenZen Loader", MB_OK | MB_ICONERROR);
|
|
}
|
|
}
|
|
return failed == 0 ? 0 : 1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
|
|
unsigned long cliPid = 0;
|
|
bool nogui = false;
|
|
bool help = false;
|
|
bool bad = false;
|
|
std::wstring badArg;
|
|
for (int i = 1; i < __argc; ++i) {
|
|
std::wstring a = __wargv[i];
|
|
if (_wcsicmp(a.c_str(), L"--nogui") == 0 ||
|
|
_wcsicmp(a.c_str(), L"/nogui") == 0) {
|
|
nogui = true;
|
|
} else if (_wcsicmp(a.c_str(), L"--help") == 0 ||
|
|
_wcsicmp(a.c_str(), L"-h") == 0 ||
|
|
_wcsicmp(a.c_str(), L"/?") == 0) {
|
|
help = true;
|
|
} else {
|
|
wchar_t* end = nullptr;
|
|
unsigned long v = wcstoul(a.c_str(), &end, 10);
|
|
if (end && *end == L'\0' && v != 0) {
|
|
cliPid = v;
|
|
} else {
|
|
bad = true;
|
|
badArg = a;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool haveCli = help || bad || cliPid != 0 || nogui;
|
|
if (haveCli) {
|
|
// Don't run the message loop below: cli.cpp holds the console-attach
|
|
// helpers too, but the GUI stays out of the headless path entirely.
|
|
AttachParentConsole();
|
|
if (help && !bad) {
|
|
Write(kUsage);
|
|
return 0;
|
|
}
|
|
if (bad) {
|
|
Write(L"Unknown argument: " + badArg + L"\n" + kUsage);
|
|
return 2;
|
|
}
|
|
if (cliPid != 0) {
|
|
// Only ever inject into a Java process: a typo'd PID must not end
|
|
// up mapping our DLL into notepad.exe.
|
|
bool isJava = false;
|
|
std::wstring image;
|
|
for (const auto& p : loader::list_java_processes()) {
|
|
if (p.pid == cliPid) {
|
|
isJava = true;
|
|
image = p.image_name;
|
|
break;
|
|
}
|
|
}
|
|
if (!isJava) {
|
|
Write(L"Injection failed: PID " + std::to_wstring(cliPid) +
|
|
L" is not a running Java (javaw.exe/java.exe) process\n");
|
|
return 1;
|
|
}
|
|
return RunHeadless(cliPid);
|
|
}
|
|
// --nogui without a PID: every Minecraft instance it can find.
|
|
return RunHeadlessAll();
|
|
}
|
|
|
|
ui::InitDpi();
|
|
ui::LogLine(L"--- loader start (gui), scale=" +
|
|
std::to_wstring(ui::g_scale));
|
|
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
|
|
ULONG_PTR gdiplusToken = 0;
|
|
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);
|
|
int rc = 0;
|
|
{
|
|
ui::SplashWindow splash;
|
|
ui::MainWindow main;
|
|
|
|
// Splash done -> reveal the main window with its entrance animation.
|
|
splash.onFinished = [&main] { main.PlayEntrance(); };
|
|
|
|
// Row Inject click -> show the progress overlay and run the real
|
|
// injection on the overlay's worker thread.
|
|
std::unique_ptr<ui::OverlayWindow> overlay;
|
|
main.onInjectRequested = [&](unsigned long pid,
|
|
const std::wstring& title) {
|
|
if (main.IsInjecting()) return;
|
|
main.SetInjectionInFlight(true);
|
|
overlay = std::make_unique<ui::OverlayWindow>();
|
|
overlay->onCompleted = [&main](bool) { main.PlayExit(); };
|
|
overlay->Show(main.hwnd(), pid, title);
|
|
};
|
|
|
|
// Main window gone -> end the process.
|
|
main.onClosed = [] { PostQuitMessage(0); };
|
|
|
|
splash.Show();
|
|
|
|
MSG msg{};
|
|
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
rc = static_cast<int>(msg.wParam);
|
|
}
|
|
Gdiplus::GdiplusShutdown(gdiplusToken);
|
|
return rc;
|
|
}
|