Files
Campus-25/static/Client/Launcher/Campus/Launcher.cpp
T
2026-10-14 06:59:51 +02:00

1236 lines
62 KiB
C++

// Campus Launcher (imgui + D3D11 edition).
//
// Merged from the parent Launcher\Launcher.cpp imgui dashboard + this folder's original
// CampusFolder.ini search + inject-CampusPatch.dll flow. Same behaviour as the GDI+ splash it
// replaces (auto-finds the game folder via CampusFolder.ini across every mounted drive, launches
// RecRoom_Release.exe, waits for GameAssembly + Referee, injects CampusPatch.dll) - just wrapped
// in the branded imgui UI.
//
// Window: pure WS_POPUP (no Windows chrome). Custom title bar with minimalist min/close.
// Body: central dark card (pink border, admin-panel style) with Screen Mode / Resolution / VR
// sections, status, LAUNCH row (green gradient pill), log tail. Background: Background.png
// stretched to fill + tiled AnimatedIcon with slow horizontal parallax.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <tlhelp32.h>
#include <dwmapi.h>
#include <d3d11.h>
#include <dxgi.h>
#include <wincodec.h>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <deque>
#include <mutex>
#include <atomic>
#include <unordered_map>
#include "vendor/imgui/imgui.h"
#include "vendor/imgui/backends/imgui_impl_win32.h"
#include "vendor/imgui/backends/imgui_impl_dx11.h"
#include "resource.h"
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "windowscodecs.lib")
#pragma comment(lib, "dwmapi.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "ole32.lib")
using std::string;
using std::vector;
// ===============================================================================================
// Resource IDs
// ===============================================================================================
#define IDR_ANIMATED_ICON_PNG 200
#define IDR_BACKGROUND_PNG 201
#define IDR_CUTOUT_PNG 202
#define IDR_RUBIK_TTF 203
#define IDR_STEAM_ICON_PNG 204
#define IDR_META_LOGO_PNG 205
// ===============================================================================================
// Config: game exe name + injected DLL relative path (both discovered under the CampusFolder.ini
// folder). Keep these names in sync with what CampusPatch.dll expects.
// ===============================================================================================
static const char* GAME_EXE_NAME = "RecRoom_Release.exe";
static const char* PATCH_DLL_REL = "CampusPatch\\Patch\\CampusPatch.dll";
// ===============================================================================================
// Runtime state
// ===============================================================================================
static string g_gameFolder; // resolved by ini search; empty until found
static string g_gameExe; // gameFolder + "\\" + GAME_EXE_NAME
static string g_dllPath; // gameFolder + "\\" + PATCH_DLL_REL
static HWND g_hwnd = nullptr;
static std::mutex g_logMtx;
static std::deque<std::string> g_logLines;
static void log_line(const char* fmt, ...) {
char body[512]; va_list ap; va_start(ap, fmt);
_vsnprintf_s(body, sizeof(body), _TRUNCATE, fmt, ap); va_end(ap);
SYSTEMTIME st; GetLocalTime(&st);
char line[600];
_snprintf_s(line, sizeof(line), _TRUNCATE, "%02d:%02d:%02d %s",
st.wHour, st.wMinute, st.wSecond, body);
std::lock_guard<std::mutex> lk(g_logMtx);
g_logLines.push_back(line);
if (g_logLines.size() > 500) g_logLines.pop_front();
OutputDebugStringA(line); OutputDebugStringA("\n");
}
// ===============================================================================================
// Filesystem
// ===============================================================================================
static string module_dir() {
char buf[MAX_PATH] = {}; GetModuleFileNameA(nullptr, buf, MAX_PATH);
string s(buf); auto slash = s.find_last_of("\\/");
return (slash == string::npos) ? string() : s.substr(0, slash);
}
static bool file_exists(const string& p) {
DWORD a = GetFileAttributesA(p.c_str());
return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY);
}
// Recursive FindFirstFile - matches the original GDI+ launcher's behaviour, including short-
// circuit return once found. Never follows reparse points (avoids junction loops on user profiles).
static bool find_file_recursive(const string& startDir, const string& fileName, string& foundPath) {
string searchPath = startDir + "\\*";
WIN32_FIND_DATAA fd{};
HANDLE hFind = FindFirstFileA(searchPath.c_str(), &fd);
if (hFind == INVALID_HANDLE_VALUE) return false;
do {
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue;
if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) continue;
string full = startDir + "\\" + fd.cFileName;
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (find_file_recursive(full, fileName, foundPath)) { FindClose(hFind); return true; }
} else if (_stricmp(fd.cFileName, fileName.c_str()) == 0) {
foundPath = full; FindClose(hFind); return true;
}
} while (FindNextFileA(hFind, &fd));
FindClose(hFind);
return false;
}
// Ported from the original Campus\Launcher.cpp - searches the launcher's own dir, its parent,
// then every mounted drive letter for CampusFolder.ini. The FOLDER containing that ini must
// also contain RecRoom_Release.exe for us to accept it.
static string find_game_folder_from_config() {
vector<string> roots;
string here = module_dir();
if (!here.empty()) roots.push_back(here);
auto slash = here.find_last_of("\\/");
if (slash != string::npos) roots.push_back(here.substr(0, slash));
DWORD drives = GetLogicalDrives();
for (int i = 0; i < 26; i++) if (drives & (1u << i)) {
char d[4] = { char('A' + i), ':', '\\', 0 };
roots.push_back(d);
}
for (const auto& r : roots) {
string found;
if (find_file_recursive(r, "CampusFolder.ini", found)) {
auto s2 = found.find_last_of("\\/");
if (s2 == string::npos) continue;
string folder = found.substr(0, s2);
if (file_exists(folder + "\\" + GAME_EXE_NAME)) return folder;
}
}
return "";
}
// ===============================================================================================
// WIC PNG loader + D3D texture
// ===============================================================================================
struct GpuTexture {
ID3D11Texture2D* tex = nullptr;
ID3D11ShaderResourceView* srv = nullptr;
int w = 0, h = 0;
void release() { if (srv) srv->Release(); if (tex) tex->Release(); srv=nullptr; tex=nullptr; }
};
static bool wic_decode_png(const uint8_t* data, size_t size,
vector<uint8_t>& outRGBA, int& outW, int& outH)
{
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
IWICImagingFactory* wic = nullptr;
if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&wic)))) return false;
IWICStream* stream = nullptr; wic->CreateStream(&stream);
if (FAILED(stream->InitializeFromMemory(const_cast<BYTE*>(data), (DWORD)size))) {
stream->Release(); wic->Release(); return false;
}
IWICBitmapDecoder* dec = nullptr;
if (FAILED(wic->CreateDecoderFromStream(stream, nullptr, WICDecodeMetadataCacheOnLoad, &dec))) {
stream->Release(); wic->Release(); return false;
}
IWICBitmapFrameDecode* frame = nullptr; dec->GetFrame(0, &frame);
UINT w = 0, hgt = 0; frame->GetSize(&w, &hgt);
IWICFormatConverter* conv = nullptr; wic->CreateFormatConverter(&conv);
conv->Initialize(frame, GUID_WICPixelFormat32bppRGBA, WICBitmapDitherTypeNone,
nullptr, 0.0, WICBitmapPaletteTypeCustom);
outW = (int)w; outH = (int)hgt;
outRGBA.assign((size_t)w * hgt * 4, 0);
conv->CopyPixels(nullptr, w * 4, (UINT)outRGBA.size(), outRGBA.data());
conv->Release(); frame->Release(); dec->Release(); stream->Release(); wic->Release();
return true;
}
// Creates a texture with a full mipmap chain and auto-generates the mips. Without this the
// Cutout logo shows aliasing when drawn at ~340px because a 1536-wide source without mips is
// linearly downsampled (still shows single-pixel jaggies). MipLevels=0 + GENERATE_MIPS +
// GenerateMips call gives us clean trilinear downscale.
static bool make_d3d_texture_from_rgba(ID3D11Device* dev, ID3D11DeviceContext* ctx,
const uint8_t* rgba, int w, int h,
GpuTexture& out) {
D3D11_TEXTURE2D_DESC td = {};
td.Width = w; td.Height = h;
td.MipLevels = 0; // 0 = full mip chain
td.ArraySize = 1;
td.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
td.SampleDesc.Count = 1;
td.Usage = D3D11_USAGE_DEFAULT;
td.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; // RT needed by GenerateMips
td.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS;
// Cannot pass initial data alongside MipLevels=0; upload level 0 separately below.
if (FAILED(dev->CreateTexture2D(&td, nullptr, &out.tex))) return false;
ctx->UpdateSubresource(out.tex, 0, nullptr, rgba, w * 4, 0);
D3D11_SHADER_RESOURCE_VIEW_DESC vd = {};
vd.Format = td.Format;
vd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
vd.Texture2D.MipLevels = (UINT)-1; // all mips
if (FAILED(dev->CreateShaderResourceView(out.tex, &vd, &out.srv))) return false;
ctx->GenerateMips(out.srv);
out.w = w; out.h = h; return true;
}
static bool find_rcdata(int rcid, const void*& out, size_t& outSz) {
HRSRC h = FindResourceA(nullptr, MAKEINTRESOURCEA(rcid), (LPCSTR)RT_RCDATA);
if (!h) return false;
HGLOBAL g = LoadResource(nullptr, h);
if (!g) return false;
out = LockResource(g); outSz = SizeofResource(nullptr, h);
return out && outSz;
}
static bool load_embedded_png_texture(ID3D11Device* dev, ID3D11DeviceContext* ctx,
int rcid, GpuTexture& out) {
const void* p; size_t n;
if (!find_rcdata(rcid, p, n)) return false;
vector<uint8_t> rgba; int w=0, hgt=0;
if (!wic_decode_png((const uint8_t*)p, n, rgba, w, hgt)) return false;
return make_d3d_texture_from_rgba(dev, ctx, rgba.data(), w, hgt, out);
}
// Silhouette variant: after decoding the PNG, walks the pixels and forces RGB=255,255,255 for
// every pixel while preserving the alpha channel. Result: a pure white cut-out of the icon
// with soft edges. When drawn via ImGui::AddImage with a colour tint, the tint multiplies the
// white and produces a clean flat-colour silhouette (which is what we need for the Steam /
// Meta logos on the cards - they need to render in a solid purple/orange, not muddied by
// the source PNG's blues).
static bool load_embedded_png_silhouette(ID3D11Device* dev, ID3D11DeviceContext* ctx,
int rcid, GpuTexture& out) {
const void* p; size_t n;
if (!find_rcdata(rcid, p, n)) return false;
vector<uint8_t> rgba; int w=0, hgt=0;
if (!wic_decode_png((const uint8_t*)p, n, rgba, w, hgt)) return false;
// Force RGB to white; keep alpha. Small caveat: pre-multiplied-alpha sources would be
// slightly off, but WIC hands us straight (non-premul) RGBA, so this is correct.
for (size_t i = 0; i + 3 < rgba.size(); i += 4) {
rgba[i + 0] = 255;
rgba[i + 1] = 255;
rgba[i + 2] = 255;
// alpha (rgba[i+3]) preserved
}
return make_d3d_texture_from_rgba(dev, ctx, rgba.data(), w, hgt, out);
}
// ===============================================================================================
// D3D11
// ===============================================================================================
static ID3D11Device* g_dev = nullptr;
static ID3D11DeviceContext* g_ctx = nullptr;
static IDXGISwapChain* g_sc = nullptr;
static ID3D11RenderTargetView* g_rtv = nullptr;
static bool create_device(HWND hwnd) {
DXGI_SWAP_CHAIN_DESC sd = {};
sd.BufferCount = 2; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = hwnd; sd.SampleDesc.Count = 1; sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
D3D_FEATURE_LEVEL fl[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0 };
D3D_FEATURE_LEVEL got;
if (FAILED(D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0,
fl, 2, D3D11_SDK_VERSION, &sd, &g_sc, &g_dev, &got, &g_ctx))) return false;
ID3D11Texture2D* back = nullptr; g_sc->GetBuffer(0, IID_PPV_ARGS(&back));
g_dev->CreateRenderTargetView(back, nullptr, &g_rtv); back->Release();
return true;
}
static void destroy_device() {
if (g_rtv) g_rtv->Release(); if (g_sc) g_sc->Release();
if (g_ctx) g_ctx->Release(); if (g_dev) g_dev->Release();
g_rtv=nullptr; g_sc=nullptr; g_ctx=nullptr; g_dev=nullptr;
}
// ===============================================================================================
// UI state
// ===============================================================================================
struct UiState {
int screenMode = 0, resIdx = 0, vrMode = 0;
bool showSettings = false;
std::string status = "Searching for Campus...";
bool statusOk = true;
bool injecting = false;
bool injectionComplete = false;
bool injectionSuccess = false;
GpuTexture faceTex, bgTex, logoTex, steamTex, metaTex;
ImFont* fontBody = nullptr;
ImFont* fontLabel = nullptr;
ImFont* fontH = nullptr;
ImFont* fontTiny = nullptr;
};
static UiState g_ui;
static const char* SCREEN_MODES[] = { "Windowed", "Fullscreen", "Borderless" };
static const char* VR_MODES[] = { "Desktop (None)", "SteamVR", "Oculus", "WMR" };
static const char* RESOLUTIONS[] = { "1920x1080", "2560x1440", "1600x900", "1280x720", "3840x2160" };
static constexpr ImU32 COL_TEXT = IM_COL32(0xF0, 0xF0, 0xF0, 255);
static constexpr ImU32 COL_TEXT_DIM = IM_COL32(0xC8, 0xC0, 0xC8, 200);
static constexpr ImU32 COL_TEXT_LABEL = IM_COL32(0xE8, 0xC0, 0xD0, 220);
static constexpr ImU32 COL_GLASS = IM_COL32(255, 255, 255, 32);
static constexpr ImU32 COL_GLASS_HI = IM_COL32(255, 255, 255, 58);
static constexpr ImU32 COL_GLASS_BORDER= IM_COL32(255, 255, 255, 90);
// ===============================================================================================
// Hover fade helper (micro-interactions)
// ===============================================================================================
static std::unordered_map<std::string, float> g_hoverFade;
static float hover_step(const std::string& key, bool hovered, float dt) {
float& v = g_hoverFade[key];
float target = hovered ? 1.0f : 0.0f;
v += (target - v) * (1.0f - expf(-8.0f * dt));
if (v < 0.001f) v = 0.0f; if (v > 0.999f) v = 1.0f;
return v;
}
static ImU32 lerp_col(ImU32 a, ImU32 b, float t) {
int ar=a&0xff, ag=(a>>8)&0xff, ab=(a>>16)&0xff, aa=(a>>24)&0xff;
int br=b&0xff, bg=(b>>8)&0xff, bb=(b>>16)&0xff, ba=(b>>24)&0xff;
return IM_COL32(ar+(int)((br-ar)*t), ag+(int)((bg-ag)*t),
ab+(int)((bb-ab)*t), aa+(int)((ba-aa)*t));
}
// ===============================================================================================
// Injection thread - ported from the GDI+ launcher. Blocks on: find folder, spawn game, wait
// for GameAssembly + Referee, VirtualAllocEx/WriteProcessMemory/CreateRemoteThread(LoadLibrary).
// ===============================================================================================
// Checks whether the target process has all of the given module names loaded RIGHT NOW.
// Case-insensitive match on the leaf module name. Returns false if we can't enumerate
// (e.g. process just spawned, snapshot not ready yet) - caller should just retry.
static bool has_modules(DWORD pid, const char* const* names, int count) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if (snap == INVALID_HANDLE_VALUE) return false;
std::vector<bool> got(count, false);
MODULEENTRY32 me{}; me.dwSize = sizeof(me);
if (Module32First(snap, &me)) do {
for (int i = 0; i < count; i++)
if (_stricmp(me.szModule, names[i]) == 0) got[i] = true;
} while (Module32Next(snap, &me));
CloseHandle(snap);
for (int i = 0; i < count; i++) if (!got[i]) return false;
return true;
}
// Stage 1: engine + anti-tamper are up. Fires early (usually < 5 s after spawn).
static bool has_engine_modules(DWORD pid) {
static const char* need[] = { "GameAssembly.dll", "Referee.dll" };
return has_modules(pid, need, 2);
}
// Stage 2: Rec Room's Steam integration is active. steam_api64.dll only loads when the
// game code hits its first Steamworks call, which in Rec Room's boot sequence happens
// during the "logging in" phase after the splash screen. Injecting AT this point means
// Referee's initial integrity pass has already run against un-patched code (so it won't
// alarm), UnityPlayer is fully initialised, and the game is about to make its first
// auth request - exactly when our SendRequest URI-rewrite hook needs to be in place.
static bool has_steam_ready(DWORD pid) {
static const char* need[] = { "steam_api64.dll" };
return has_modules(pid, need, 1);
}
// Legacy wrapper kept so old call sites don't need to change.
static bool has_all_modules(DWORD pid) { return has_engine_modules(pid); }
static void set_status(const char* s, bool ok = true) {
g_ui.status = s; g_ui.statusOk = ok;
log_line("%s", s);
}
static DWORD WINAPI inject_thread(LPVOID) {
g_ui.injecting = true;
set_status("Searching for Campus...");
Sleep(300);
g_gameFolder = find_game_folder_from_config();
if (g_gameFolder.empty()) {
set_status("ERROR: CampusFolder.ini not found on any drive", false);
g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false;
return 1;
}
g_gameExe = g_gameFolder + "\\" + GAME_EXE_NAME;
g_dllPath = g_gameFolder + "\\" + PATCH_DLL_REL;
log_line("Found game folder: %s", g_gameFolder.c_str());
Sleep(200);
if (!file_exists(g_gameExe)) { set_status("ERROR: RecRoom_Release.exe missing in game folder", false); g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false; return 1; }
if (!file_exists(g_dllPath)) { set_status("ERROR: CampusPatch.dll missing", false); g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false; return 1; }
set_status("Starting game...");
Sleep(200);
STARTUPINFOA si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{};
// Force Unity to skip all XR / VR initialisation. On this install SteamVR reports
// VRInitError_Init_InstallationCorrupt on every launch, and Rec Room's fallback
// path from a failed VR init is unstable enough to have been implicated in the
// vanilla-client crash. `-nohmd` = "no HMD, run flat"; `-window-mode exclusive`
// tells Unity to skip windowed-mode probing. Both are stock Unity command-line
// flags, no Rec Room integration required.
//
// CreateProcessA requires lpCommandLine to be a writable buffer and, when
// lpApplicationName is set, to still start with the exe (becomes argv[0]).
// Quote the exe path in case it lives under a folder with spaces.
std::string cmdline = "\"" + g_gameExe + "\" -nohmd -window-mode exclusive";
std::vector<char> cmdBuf(cmdline.begin(), cmdline.end());
cmdBuf.push_back('\0');
// Build a scrubbed environment block for the child. Windows Event Log has repeatedly
// flagged `tier0_s64.dll` (Steam Overlay's proxy runtime) as the crashing module on
// this build. Steam Overlay uses env vars like SteamAppId / SteamGameId /
// SteamOverlayGameId to decide whether to inject into a child process. Wiping them
// from OUR child means Steam sees the process as "not a Steam game" and skips the
// overlay hook - the game still finds steam_api64.dll and does its own SteamAPI_Init,
// so account auth is unaffected. Fallback: if BuildScrubbedEnv() ever fails we let
// the child inherit our env, which is the historical behaviour.
auto BuildScrubbedEnv = []() -> std::vector<char> {
std::vector<char> block;
LPCH env = GetEnvironmentStringsA();
if (!env) return block;
static const char* strip[] = {
"SteamAppId=", "SteamGameId=", "SteamOverlayGameId=",
"SteamOverlayNativeAppId=", "SteamClientLaunch=", "SteamNoOverlayUIDrawing=",
"SteamEnv=", "SteamPath=", "SteamUniverse=", "SteamController="
};
LPCH cur = env;
while (*cur) {
size_t len = strlen(cur);
bool drop = false;
for (const char* pfx : strip) {
size_t pl = strlen(pfx);
if (len >= pl && _strnicmp(cur, pfx, (int)pl) == 0) { drop = true; break; }
}
if (!drop) {
block.insert(block.end(), cur, cur + len);
block.push_back('\0');
}
cur += len + 1;
}
block.push_back('\0'); // double-null terminator per Win32 env block spec
FreeEnvironmentStringsA(env);
return block;
};
std::vector<char> childEnv = BuildScrubbedEnv();
LPVOID envPtr = childEnv.empty() ? nullptr : childEnv.data();
if (!CreateProcessA(g_gameExe.c_str(), cmdBuf.data(), nullptr, nullptr, FALSE, 0,
envPtr, g_gameFolder.c_str(), &si, &pi)) {
set_status("ERROR: CreateProcess failed", false);
g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false;
return 2;
}
log_line("Child env scrubbed of Steam vars (blocks Overlay attach path)");
log_line("Spawned PID=%lu with -nohmd, waiting for GameAssembly + Referee...", pi.dwProcessId);
// Poll for the game's engine modules to appear. GameAssembly.dll + Referee.dll are the
// last modules to load during Unity's early bootstrap - once they're both present the
// process is stable enough to accept a CreateRemoteThread inject. Timeout at 30 s so
// a crashing / non-loading game surfaces as a clear error instead of hanging forever.
int waited = 0;
while (waited < 30000) {
DWORD exitCode = 0;
if (GetExitCodeProcess(pi.hProcess, &exitCode) && exitCode != STILL_ACTIVE) {
set_status("ERROR: Game crashed before modules loaded", false);
CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false;
return 3;
}
if (has_engine_modules(pi.dwProcessId)) break;
Sleep(100); waited += 100;
if (waited % 1000 == 0) {
char b[96]; _snprintf_s(b, sizeof(b), _TRUNCATE, "Waiting for engine... (%d/30000 ms)", waited);
set_status(b);
}
}
if (waited >= 30000) {
set_status("ERROR: Engine module load timeout", false);
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false;
return 4;
}
log_line("Modules loaded after %d ms - injecting CampusPatch.dll immediately", waited);
// Formerly waited for steam_api64.dll (stage 2) here - reverted per user request to
// inject as early as possible so our SendRequest URL rewrite hook is live BEFORE the
// client's first HTTP call goes out.
set_status("Loading Patch...");
HMODULE k32 = GetModuleHandleA("kernel32.dll");
FARPROC pLoad = GetProcAddress(k32, "LoadLibraryA");
LPVOID remote = VirtualAllocEx(pi.hProcess, nullptr, g_dllPath.size() + 1,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!remote) { set_status("ERROR: VirtualAllocEx failed", false); g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false; return 5; }
WriteProcessMemory(pi.hProcess, remote, g_dllPath.c_str(), g_dllPath.size() + 1, nullptr);
HANDLE ht = CreateRemoteThread(pi.hProcess, nullptr, 0,
(LPTHREAD_START_ROUTINE)pLoad, remote, 0, nullptr);
if (!ht) { set_status("ERROR: CreateRemoteThread failed", false); g_ui.injecting = false; g_ui.injectionComplete = true; g_ui.injectionSuccess = false; return 6; }
WaitForSingleObject(ht, 15000);
DWORD injectExit = 0; GetExitCodeThread(ht, &injectExit);
CloseHandle(ht); VirtualFreeEx(pi.hProcess, remote, 0, MEM_RELEASE);
if (injectExit == 0) {
set_status("Failed to load - check logs", false);
g_ui.injectionSuccess = false;
} else {
set_status("Enjoy, made by Kyle");
g_ui.injectionSuccess = true;
}
g_ui.injecting = false; g_ui.injectionComplete = true;
CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
return 0;
}
// ===============================================================================================
// Background - Background.png full-window + tiled AnimatedIcon with slow parallax
// ===============================================================================================
static void draw_background(ImDrawList* dl, ImVec2 sz, float t) {
if (g_ui.bgTex.srv) {
dl->AddImage((ImTextureID)g_ui.bgTex.srv, ImVec2(0,0), sz,
ImVec2(0,0), ImVec2(1,1), IM_COL32(255,255,255,255));
} else {
dl->AddRectFilledMultiColor(ImVec2(0,0), sz,
IM_COL32(0x79,0x1B,0x8D,255), IM_COL32(0xC7,0x2A,0x5C,255),
IM_COL32(0xFA,0x99,0x12,255), IM_COL32(0xBF,0x29,0x66,255));
}
if (g_ui.faceTex.srv) {
const float scale = 0.28f;
float iw = g_ui.faceTex.w * scale, ih = g_ui.faceTex.h * scale;
const float step = 128.0f; // wider spacing = calmer
float offX = fmodf(t * (step / 40.0f), step); // slower drift too
ImU32 tint = IM_COL32(255, 255, 255, 35); // was 60 - much softer now
for (float y = -ih; y < sz.y + ih; y += step) {
float xoff = ((int)(y / step) % 2) ? step * 0.5f : 0.0f;
for (float x = -iw + xoff - step + offX; x < sz.x + iw; x += step) {
dl->AddImage((ImTextureID)g_ui.faceTex.srv,
ImVec2(x, y), ImVec2(x + iw, y + ih),
ImVec2(0,0), ImVec2(1,1), tint);
}
}
}
}
// ===============================================================================================
// Custom title bar - dark alpha gradient + minimalist glyphs
// ===============================================================================================
static const float TITLEBAR_H = 40.0f;
static void draw_titlebar(ImDrawList* dl, ImVec2 sz) {
dl->AddRectFilledMultiColor(ImVec2(0,0), ImVec2(sz.x, TITLEBAR_H),
IM_COL32(0, 0, 0, 130), IM_COL32(0, 0, 0, 130),
IM_COL32(0, 0, 0, 0), IM_COL32(0, 0, 0, 0));
const float bw = 46.0f, bh = TITLEBAR_H;
float dt = ImGui::GetIO().DeltaTime;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::SetCursorScreenPos(ImVec2(sz.x - 2 * bw, 0));
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(255,255,255,30));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(255,255,255,60));
if (ImGui::Button("##min", ImVec2(bw, bh))) ShowWindow(g_hwnd, SW_MINIMIZE);
bool minH = ImGui::IsItemHovered();
ImGui::PopStyleColor(3);
{
float a = 180 + (int)(75 * hover_step("min_glyph", minH, dt));
ImVec2 c(sz.x - 2 * bw + bw * 0.5f, bh * 0.5f + 6.0f);
dl->AddLine(ImVec2(c.x - 6, c.y), ImVec2(c.x + 6, c.y),
IM_COL32(240, 240, 240, (int)a), 1.4f);
}
ImGui::SetCursorScreenPos(ImVec2(sz.x - bw, 0));
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(220, 60, 60, 220));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(240, 80, 80, 240));
if (ImGui::Button("##close", ImVec2(bw, bh))) PostMessage(g_hwnd, WM_CLOSE, 0, 0);
bool clsH = ImGui::IsItemHovered();
ImGui::PopStyleColor(3);
{
float a = 180 + (int)(75 * hover_step("close_glyph", clsH, dt));
ImVec2 c(sz.x - bw * 0.5f, bh * 0.5f);
dl->AddLine(ImVec2(c.x - 5.5f, c.y - 5.5f), ImVec2(c.x + 5.5f, c.y + 5.5f),
IM_COL32(240, 240, 240, (int)a), 1.4f);
dl->AddLine(ImVec2(c.x - 5.5f, c.y + 5.5f), ImVec2(c.x + 5.5f, c.y - 5.5f),
IM_COL32(240, 240, 240, (int)a), 1.4f);
}
ImGui::PopStyleVar(2);
ImVec2 mp = ImGui::GetIO().MousePos;
bool inDrag = mp.y >= 0 && mp.y < TITLEBAR_H && mp.x >= 0 && mp.x < sz.x - 2 * bw;
if (inDrag && ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered()) {
ReleaseCapture();
SendMessage(g_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
}
}
// ===============================================================================================
// Logo - Cutout.png centered with soft glow + drop shadow
// ===============================================================================================
static const float LOGO_TARGET_W = 260.0f; // was 340 - shrunk so cards get more vertical room
static float logo_actual_h() {
if (!g_ui.logoTex.srv || g_ui.logoTex.w == 0) return 100.0f;
return LOGO_TARGET_W * ((float)g_ui.logoTex.h / (float)g_ui.logoTex.w);
}
static void draw_logo(ImDrawList* dl, ImVec2 sz) {
const float topY = TITLEBAR_H + 16.0f;
float lw = LOGO_TARGET_W, lh = logo_actual_h();
float lx = floorf((sz.x - lw) * 0.5f);
float ly = floorf(topY);
if (!g_ui.logoTex.srv) return;
ImTextureID tex = (ImTextureID)g_ui.logoTex.srv;
// Two-pass soft shadow (wider fainter + tighter darker) for a real "lifted" look.
dl->AddImage(tex,
ImVec2(lx - 2, ly + 10), ImVec2(lx + lw + 2, ly + lh + 10),
ImVec2(0,0), ImVec2(1,1), IM_COL32(0, 0, 0, 45));
dl->AddImage(tex,
ImVec2(lx + 1, ly + 5), ImVec2(lx + lw + 1, ly + lh + 5),
ImVec2(0,0), ImVec2(1,1), IM_COL32(0, 0, 0, 90));
// Main logo with mipmapped downscale = clean edges.
dl->AddImage(tex,
ImVec2(lx, ly), ImVec2(lx + lw, ly + lh),
ImVec2(0,0), ImVec2(1,1), IM_COL32(255, 255, 255, 255));
// (Previous "MADE BY KYLE" subtitle removed - byline now lives in the footer inside the
// panel as "Campus · Made by Kyle", so this floating copy was redundant.)
}
static float logo_bottom_y() {
return TITLEBAR_H + 16.0f + logo_actual_h() + 18.0f;
}
// ===============================================================================================
// Glass controls
// ===============================================================================================
static void draw_chevron(ImDrawList* dl, ImVec2 c, ImU32 col) {
dl->AddLine(ImVec2(c.x-4, c.y-2), ImVec2(c.x, c.y+3), col, 1.6f);
dl->AddLine(ImVec2(c.x+4, c.y-2), ImVec2(c.x, c.y+3), col, 1.6f);
}
// Dropdown that blends into the unified dashboard: same dark fill as the container, white text,
// white chevron, rounded to match the container's outer corners, no clashing blue border.
static void glass_combo(const char* label, int* v, const char* const items[], int n) {
// Frame (closed state) - matches the dashboard's darker inner cells.
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(28, 20, 38, 220));
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, IM_COL32(42, 30, 55, 235));
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, IM_COL32(50, 36, 65, 240));
ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(255, 255, 255, 40));
// Popup (open state) - also dark so the whole widget reads as one system.
ImGui::PushStyleColor(ImGuiCol_PopupBg, IM_COL32(22, 15, 32, 240));
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(240, 240, 240, 255));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(255, 150, 205, 60));
ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(255, 150, 205, 90));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32(255, 150, 205, 130));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 10.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 10.0f);
ImGui::Combo(label, v, items, n);
// Overpaint the default arrow area with our own dark chevron so nothing clashes.
ImVec2 mx = ImGui::GetItemRectMax(); ImVec2 mn = ImGui::GetItemRectMin();
float arrowW = 22.0f;
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(ImVec2(mx.x - arrowW - 1, mn.y + 1), ImVec2(mx.x - 1, mx.y - 1),
IM_COL32(28, 20, 38, 255), 8.0f);
draw_chevron(dl, ImVec2(mx.x - arrowW*0.5f - 1, (mn.y + mx.y)*0.5f),
IM_COL32(240, 240, 240, 220));
ImGui::PopStyleVar(3); ImGui::PopStyleColor(9);
}
static void glass_radio(const char* label, int* v, int val) {
ImGui::PushStyleColor(ImGuiCol_FrameBg, COL_GLASS);
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, COL_GLASS_HI);
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, COL_GLASS_HI);
ImGui::PushStyleColor(ImGuiCol_CheckMark, IM_COL32(255, 210, 120, 255));
ImGui::PushStyleColor(ImGuiCol_Border, COL_GLASS_BORDER);
ImGui::PushStyleColor(ImGuiCol_Text, COL_TEXT);
ImGui::RadioButton(label, v, val);
ImGui::PopStyleColor(6);
}
// ===============================================================================================
// LAUNCH button - hand-drawn green gradient pill with pulse + hover fade + top highlight
// ===============================================================================================
static bool draw_launch_button(ImVec2 pos, ImVec2 size, float t, bool ready) {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 br(pos.x + size.x, pos.y + size.y);
ImGui::SetCursorScreenPos(pos);
ImGui::InvisibleButton("##launch", size);
bool hovered = ImGui::IsItemHovered();
bool active = ImGui::IsItemActive();
bool clicked = ImGui::IsItemClicked();
float hv = hover_step("launch_hover", hovered, ImGui::GetIO().DeltaTime);
float pulse = 0.5f + 0.5f * sinf(t * 2.2f);
if (!ready || hovered) pulse = 0.0f;
if (pulse > 0.02f) {
int a = 30 + (int)(50 * pulse);
for (int i = 4; i >= 1; i--) {
dl->AddRect(ImVec2(pos.x - i, pos.y - i), ImVec2(br.x + i, br.y + i),
IM_COL32(120, 240, 130, a / i), 14.0f + i, 0, 1.0f);
}
}
ImU32 topBase = IM_COL32( 96, 220, 108, 255); // brighter lime top
ImU32 midBase = IM_COL32( 62, 175, 78, 255); // mid stop for smoother gradient
ImU32 botBase = IM_COL32( 24, 110, 45, 255); // deep forest bottom
ImU32 topHover = IM_COL32(120, 240, 130, 255);
ImU32 midHover = IM_COL32( 85, 200, 100, 255);
ImU32 botHover = IM_COL32( 40, 140, 60, 255);
ImU32 topPress = IM_COL32( 75, 185, 85, 255);
ImU32 midPress = IM_COL32( 45, 140, 60, 255);
ImU32 botPress = IM_COL32( 20, 90, 35, 255);
ImU32 topC = active ? topPress : lerp_col(topBase, topHover, hv);
ImU32 midC = active ? midPress : lerp_col(midBase, midHover, hv);
ImU32 botC = active ? botPress : lerp_col(botBase, botHover, hv);
// Two-stop vertical gradient for smoother depth (top->mid, mid->bot).
float midY = pos.y + size.y * 0.55f;
dl->AddRectFilledMultiColor(pos, ImVec2(br.x, midY), topC, topC, midC, midC);
dl->AddRectFilledMultiColor(ImVec2(pos.x, midY), br, midC, midC, botC, botC);
// Rounded 1.5px border - drawn on top clips corner "bleed" from the multi-color fill.
dl->AddRect(pos, br, IM_COL32(255, 255, 255, 100), 12.0f, 0, 1.5f);
// Bright inner top edge (specular highlight) - shortened to top ~30% for realism.
dl->AddRectFilledMultiColor(
ImVec2(pos.x + 2, pos.y + 1), ImVec2(br.x - 2, pos.y + size.y * 0.32f),
IM_COL32(255,255,255,110), IM_COL32(255,255,255,110),
IM_COL32(255,255,255,0), IM_COL32(255,255,255,0));
// Inner bottom shadow - the button sits INTO its own base slightly.
dl->AddRectFilledMultiColor(
ImVec2(pos.x + 2, br.y - size.y * 0.25f), ImVec2(br.x - 2, br.y - 1),
IM_COL32(0,0,0,0), IM_COL32(0,0,0,0),
IM_COL32(0,0,0,90), IM_COL32(0,0,0,90));
ImGui::PushFont(g_ui.fontLabel);
const char* label = g_ui.injecting ? "LAUNCHING..." : "LAUNCH";
ImVec2 ts = ImGui::CalcTextSize(label);
dl->AddText(g_ui.fontLabel, g_ui.fontLabel->FontSize,
ImVec2(pos.x + (size.x - ts.x)*0.5f + 1, pos.y + (size.y - ts.y)*0.5f + 2),
IM_COL32(0, 40, 20, 130), label);
dl->AddText(g_ui.fontLabel, g_ui.fontLabel->FontSize,
ImVec2(pos.x + (size.x - ts.x)*0.5f, pos.y + (size.y - ts.y)*0.5f),
IM_COL32(255, 255, 255, 255), label);
ImGui::PopFont();
return clicked && !g_ui.injecting;
}
// ===============================================================================================
// Small glass pill button - used for Exit / Uninstall / Repair in the Meow.Net-style header
// and footer rows. Rose-translucent, hover-fades to brighter, subtle white border.
// ===============================================================================================
// Glass pill button. Per spec:
// default background: rgba(255,255,255,0.05) alpha 13
// default border: 1px solid rgba(255,255,255,0.3) alpha 76
// tintFill != 0 overrides the fill (used by Uninstall for its red warning tint), same for
// tintBorder. Hover fades the fill up ~10 alpha for a subtle micro-interaction.
static bool draw_pill_button(const char* label, ImVec2 pos, ImVec2 size,
ImU32 tintFill, ImU32 tintBorder) {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 br(pos.x + size.x, pos.y + size.y);
ImGui::SetCursorScreenPos(pos);
ImGui::InvisibleButton(label, size);
bool hovered = ImGui::IsItemHovered();
bool clicked = ImGui::IsItemClicked();
float hv = hover_step(std::string("pill_") + label, hovered, ImGui::GetIO().DeltaTime);
ImU32 fill;
if (tintFill) {
// Use provided fill but push its alpha up on hover for feedback.
int ta = (tintFill >> 24) & 0xff;
int ba = ta + (int)(30 * hv);
if (ba > 255) ba = 255;
fill = (tintFill & 0x00FFFFFF) | ((ba & 0xff) << 24);
} else {
// Spec default: 5% -> 15% on hover
int fillA = 13 + (int)(25 * hv);
fill = IM_COL32(255, 255, 255, fillA);
}
dl->AddRectFilled(pos, br, fill, 10.0f);
// Secondary-button border thinner per fix #5 - default alpha lowered 76 -> 50 so these
// pills visually recede next to the LAUNCH buttons' brighter 60% white border.
dl->AddRect (pos, br, tintBorder ? tintBorder : IM_COL32(255, 255, 255, 50),
10.0f, 0, 1.0f);
ImVec2 ts = ImGui::CalcTextSize(label);
dl->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(pos.x + (size.x - ts.x)*0.5f + 1, pos.y + (size.y - ts.y)*0.5f + 1),
IM_COL32(0, 0, 0, 110), label);
dl->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(pos.x + (size.x - ts.x)*0.5f, pos.y + (size.y - ts.y)*0.5f),
IM_COL32(255, 255, 255, 245), label);
return clicked;
}
// ===============================================================================================
// UI
// ===============================================================================================
static void draw_ui() {
ImGuiIO& io = ImGui::GetIO();
ImVec2 sz = io.DisplaySize;
float t = (float)ImGui::GetTime();
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(sz);
ImGui::PushStyleColor(ImGuiCol_WindowBg, IM_COL32(0,0,0,0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0,0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::Begin("root", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus);
draw_background(ImGui::GetBackgroundDrawList(), sz, t);
ImDrawList* fg = ImGui::GetWindowDrawList();
// Big CAMPUS Cutout logo at the top - restored per operator request. Sits between the
// title bar and the outer Meow.Net-style panel.
draw_logo(fg, sz);
draw_titlebar(fg, sz);
ImGui::PushFont(g_ui.fontBody);
ImGui::PushStyleColor(ImGuiCol_Text, COL_TEXT);
// ---------- Meow.Net-style outer container ---------------------------------------------
// Wider outer margin (fix #6) so the glass panel doesn't crowd the window edges. 26 -> 40.
const float outerPad = 40.0f;
const float outerX = outerPad;
const float outerY = logo_bottom_y();
const float outerW = sz.x - outerPad * 2;
const float outerH = sz.y - outerY - outerPad;
ImVec2 oPos(outerX, outerY);
ImVec2 oBR (outerX + outerW, outerY + outerH);
// Soft outer shadow for lift.
fg->AddRectFilled(ImVec2(oPos.x + 4, oPos.y + 10), ImVec2(oBR.x + 4, oBR.y + 12),
IM_COL32(0, 0, 0, 60), 22.0f);
// Glassmorphism body - dark, semi-transparent so the underlying purple-to-orange gradient
// still shows through, but darker than before for stronger content contrast.
fg->AddRectFilled(oPos, oBR, IM_COL32(12, 6, 18, 130), 20.0f);
// Top-inner highlight - thin white sheen at the top edge for that "lit from above" feel.
fg->AddRectFilledMultiColor(
ImVec2(oPos.x + 1.5f, oPos.y + 1.5f), ImVec2(oBR.x - 1.5f, oPos.y + 60),
IM_COL32(255, 255, 255, 42), IM_COL32(255, 255, 255, 42),
IM_COL32(255, 255, 255, 0), IM_COL32(255, 255, 255, 0));
// Thin white hairline border - the frosted-glass edge signature.
fg->AddRect(oPos, oBR, IM_COL32(255, 255, 255, 120), 20.0f, 0, 1.2f);
// ---------- Header row (icon + title/subtitle + Exit) -----------------------------------
const float hpad = 28.0f;
const float iconSz = 60.0f;
const float headerY = oPos.y + 26.0f;
const float iconX = oPos.x + hpad;
// Header Campus chip - vivid RED square (#E42C3E) with:
// - soft top-inner white highlight (raised glass look)
// - 1px bright-red INNER glow inset 2px from the outer edge (fix #1c)
// - crisp white outer border
fg->AddRectFilled(ImVec2(iconX, headerY),
ImVec2(iconX + iconSz, headerY + iconSz),
IM_COL32(0xE4, 0x2C, 0x3E, 235), 12.0f);
fg->AddRectFilledMultiColor(
ImVec2(iconX + 1, headerY + 1),
ImVec2(iconX + iconSz - 1, headerY + iconSz * 0.5f),
IM_COL32(255, 255, 255, 60), IM_COL32(255, 255, 255, 60),
IM_COL32(255, 255, 255, 0), IM_COL32(255, 255, 255, 0));
// Bright red inner glow - a hairline inset that catches light on the chip edge.
fg->AddRect (ImVec2(iconX + 2, headerY + 2),
ImVec2(iconX + iconSz - 2, headerY + iconSz - 2),
IM_COL32(255, 90, 110, 180), 10.0f, 0, 1.0f);
fg->AddRect (ImVec2(iconX, headerY),
ImVec2(iconX + iconSz, headerY + iconSz),
IM_COL32(255, 255, 255, 140), 12.0f, 0, 1.2f);
if (g_ui.faceTex.srv) {
float inset = 8.0f;
fg->AddImage((ImTextureID)g_ui.faceTex.srv,
ImVec2(iconX + inset, headerY + inset),
ImVec2(iconX + iconSz - inset, headerY + iconSz - inset),
ImVec2(0,0), ImVec2(1,1), IM_COL32(255, 255, 255, 255));
}
// Title + subtitle
float textX = iconX + iconSz + 16.0f;
if (g_ui.fontH) {
const char* title = "Campus Launcher";
fg->AddText(g_ui.fontH, g_ui.fontH->FontSize,
ImVec2(textX + 1, headerY + 3 + 1), IM_COL32(0, 0, 0, 100), title);
fg->AddText(g_ui.fontH, g_ui.fontH->FontSize,
ImVec2(textX, headerY + 3), IM_COL32(255, 255, 255, 255), title);
}
// Live subtitle - normally "Select your play mode", replaced with status while injecting.
// Muted white per new spec (rgba(255,255,255,0.6) -> alpha 153) so main titles pop more.
const char* subtitle = g_ui.status.c_str();
if (!g_ui.injecting && !g_ui.injectionComplete) subtitle = "Select your play mode";
fg->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(textX, headerY + 40),
g_ui.statusOk ? IM_COL32(255, 255, 255, 153) : IM_COL32(255, 190, 200, 230),
subtitle);
// Exit pill - vertically centered with the "Campus Launcher" title text specifically
// (per spec) instead of the icon block. Title Y-center = headerY + 3 + fontH/2.
const float exitW = 84.0f, exitH = 36.0f;
float titleFontH = g_ui.fontH ? g_ui.fontH->FontSize : 28.0f;
float titleCenterY = headerY + 3.0f + titleFontH * 0.5f;
ImVec2 exitPos(oBR.x - hpad - exitW, floorf(titleCenterY - exitH * 0.5f));
if (draw_pill_button("Exit", exitPos, ImVec2(exitW, exitH), 0, 0))
PostMessage(g_hwnd, WM_CLOSE, 0, 0);
// Divider under header - thin white
float divY = headerY + iconSz + 22.0f;
fg->AddLine(ImVec2(oPos.x + hpad, divY), ImVec2(oBR.x - hpad, divY),
IM_COL32(255, 255, 255, 90), 1.0f);
// ---------- Two play-mode cards ---------------------------------------------------------
// Space between divider and footer row, split in half horizontally.
const float footerRowH = 62.0f; // brand + Uninstall + Repair
const float cardsTop = divY + 22.0f;
const float cardsBot = oBR.y - footerRowH;
const float cardsGap = 16.0f;
const float cardW = (outerW - hpad * 2 - cardsGap) * 0.5f;
const float cardH = cardsBot - cardsTop;
// Renders a Screen/VR play-mode card. `iconDraw` is a lambda that draws a vector icon
// (monitor or headset) as a solid flat colour in the top-left, replacing the earlier
// colored-chip-with-PNG approach - the PNG icons rendered muddy/pixelated at 40px, while
// hand-drawn primitives are crisp and clearly recolourable. `iconColor` is the flat fill.
auto draw_mode_card = [&](const char* title, const char* subtitle, const char* btnLabel,
ImVec2 pos, ImVec2 size,
auto iconDraw, ImU32 iconColor, auto onClick) {
ImVec2 br(pos.x + size.x, pos.y + size.y);
// Frosted glass card per spec:
// background: rgba(255,255,255,0.03) -> IM_COL32(255,255,255, 8)
// border: 1px solid rgba(255,255,255,0.15) -> IM_COL32(255,255,255, 38)
fg->AddRectFilled(pos, br, IM_COL32(255, 255, 255, 8), 14.0f);
// Subtle top-inner sheen kept for depth (a hair of white at the top edge).
fg->AddRectFilledMultiColor(
ImVec2(pos.x + 1, pos.y + 1), ImVec2(br.x - 1, pos.y + 40),
IM_COL32(255, 255, 255, 25), IM_COL32(255, 255, 255, 25),
IM_COL32(255, 255, 255, 0), IM_COL32(255, 255, 255, 0));
fg->AddRect(pos, br, IM_COL32(255, 255, 255, 38), 14.0f, 0, 1.0f);
// Coloured chip BEHIND the native-colour icon. The chip supplies the brand-adjacent
// colour context (purple for Screen, orange for VR) that ties the card into the
// background gradient, while the icon on top keeps all its native detail.
const float icSz = 56.0f;
ImVec2 icPos(pos.x + 22.0f, pos.y + 20.0f);
ImVec2 icBR (icPos.x + icSz, icPos.y + icSz);
// soft drop shadow beneath the chip
fg->AddRectFilled(ImVec2(icPos.x + 1, icPos.y + 4), ImVec2(icBR.x + 1, icBR.y + 4),
IM_COL32(0, 0, 0, 60), 12.0f);
// chip body (iconColor is the flat brand tint)
fg->AddRectFilled(icPos, icBR, iconColor, 12.0f);
// top-inner white highlight for the raised-tile feel
fg->AddRectFilledMultiColor(
ImVec2(icPos.x + 1, icPos.y + 1), ImVec2(icBR.x - 1, icPos.y + icSz * 0.5f),
IM_COL32(255, 255, 255, 55), IM_COL32(255, 255, 255, 55),
IM_COL32(255, 255, 255, 0), IM_COL32(255, 255, 255, 0));
// thin white glass border
fg->AddRect (icPos, icBR, IM_COL32(255, 255, 255, 120), 12.0f, 0, 1.2f);
// Icon sits ON TOP of the chip, in its native colours (Steam blue+white, Meta blue).
// iconDraw does aspect-preserved fit inside a 44x44 inner box (icSz - 12 = 12px pad).
const float iconPad = 10.0f;
ImVec2 iconInnerPos(icPos.x + iconPad, icPos.y + iconPad);
iconDraw(fg, iconInnerPos, icSz - iconPad * 2, IM_COL32(255,255,255,255));
// Tightened typography (fix #3 - was too much whitespace):
// title at pos.y + 80 (was 92)
// subtitle at pos.y + 116 (was 130)
// Subtitle uses muted white alpha 153 (0.6) per new spec (was 178).
if (g_ui.fontH) {
fg->AddText(g_ui.fontH, g_ui.fontH->FontSize,
ImVec2(pos.x + 22, pos.y + 80),
IM_COL32(255, 255, 255, 255), title);
}
fg->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(pos.x + 22, pos.y + 116),
IM_COL32(255, 255, 255, 153), subtitle);
// LAUNCH pill anchored to the bottom of the card, closer to subtitle now.
const float bh = 42.0f;
const float bMargin = 18.0f;
float bw = size.x - bMargin * 2;
ImVec2 bPos(pos.x + bMargin, br.y - bh - bMargin);
ImVec2 bBR (bPos.x + bw, bPos.y + bh);
ImGui::SetCursorScreenPos(bPos);
ImGui::InvisibleButton(btnLabel, ImVec2(bw, bh));
bool hov = ImGui::IsItemHovered();
bool clk = ImGui::IsItemClicked();
float hv = hover_step(std::string("play_") + btnLabel, hov, ImGui::GetIO().DeltaTime);
// LAUNCH button per spec:
// background: rgba(255,255,255,0.1) -> alpha 26
// hover: rgba(255,255,255,0.2) -> alpha 51
// border: 1px solid rgba(255,255,255,0.6) -> alpha 153
// text + play triangle: pure white
int fillA = 26 + (int)(25 * hv);
fg->AddRectFilled(bPos, bBR, IM_COL32(255, 255, 255, fillA), 12.0f);
fg->AddRect (bPos, bBR, IM_COL32(255, 255, 255, 153), 12.0f, 0, 1.0f);
// Play triangle + label, both bright white. Triangle drawn via AddConvexPolyFilled
// (fix #2 - larger, crisper vector triangle with proper AA rather than the earlier
// small AddTriangleFilled). Sized to match the label's cap-height so it reads as a
// proper play glyph, not a bullet.
{
ImVec2 ts = ImGui::CalcTextSize(btnLabel);
const float triH = ts.y * 0.90f; // slightly under cap-height
const float triW = triH * 0.85f; // narrower isoceles play arrow
const float triGap = 12.0f;
float groupW = triW + triGap + ts.x;
float baseX = floorf(bPos.x + (bw - groupW) * 0.5f);
float baseY = bPos.y + bh * 0.5f;
ImVec2 pts[3] = {
ImVec2(baseX, baseY - triH * 0.5f),
ImVec2(baseX + triW, baseY),
ImVec2(baseX, baseY + triH * 0.5f),
};
ImVec2 ptsShadow[3] = {
ImVec2(pts[0].x + 1, pts[0].y + 1),
ImVec2(pts[1].x + 1, pts[1].y + 1),
ImVec2(pts[2].x + 1, pts[2].y + 1),
};
fg->AddConvexPolyFilled(ptsShadow, 3, IM_COL32(0, 0, 0, 110));
fg->AddConvexPolyFilled(pts, 3, IM_COL32(255, 255, 255, 255));
float labelX = baseX + triW + triGap;
float labelY = bPos.y + (bh - ts.y) * 0.5f;
fg->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(labelX + 1, labelY + 1),
IM_COL32(0, 0, 0, 110), btnLabel);
fg->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(labelX, labelY),
IM_COL32(255, 255, 255, 255), btnLabel);
}
if (clk && !g_ui.injecting) onClick();
};
// Icon drawer: paint the given texture at its NATIVE colours, aspect-preserved, centred
// in the 56x56 box. Tint colour parameter is now just for API compatibility (kept white
// = no colour multiply) - the PNGs render as-is with their real Steam/Meta artwork.
auto draw_native_icon = [](ImDrawList* d, ImVec2 pos, float sz, ImU32 /*col*/,
GpuTexture* tex) {
if (!tex || !tex->srv) return;
float ar = (float)tex->w / (float)tex->h;
float boxW = sz, boxH = sz;
float dw = boxW, dh = boxH;
if (ar > 1.0f) dh = dw / ar; else dw = dh * ar;
float dx = floorf(pos.x + (sz - dw) * 0.5f);
float dy = floorf(pos.y + (sz - dh) * 0.5f);
d->AddImage((ImTextureID)tex->srv,
ImVec2(dx, dy), ImVec2(dx + dw, dy + dh),
ImVec2(0,0), ImVec2(1,1), IM_COL32(255, 255, 255, 255));
};
ImVec2 scPos(oPos.x + hpad, cardsTop);
draw_mode_card("Screen", "Play in Screen Mode", "LAUNCH SCREEN",
scPos, ImVec2(cardW, cardH),
[&draw_native_icon](ImDrawList* d, ImVec2 p, float s, ImU32 c) {
draw_native_icon(d, p, s, c, &g_ui.steamTex);
},
IM_COL32(255, 255, 255, 255), // ignored (icon renders native)
[]{
HANDLE th = CreateThread(nullptr, 0, inject_thread, nullptr, 0, nullptr);
if (th) CloseHandle(th);
});
ImVec2 vrPos(scPos.x + cardW + cardsGap, cardsTop);
draw_mode_card("VR", "Play in VR", "LAUNCH VR",
vrPos, ImVec2(cardW, cardH),
[&draw_native_icon](ImDrawList* d, ImVec2 p, float s, ImU32 c) {
draw_native_icon(d, p, s, c, &g_ui.metaTex);
},
IM_COL32(255, 255, 255, 255), // ignored (icon renders native)
[]{
HANDLE th = CreateThread(nullptr, 0, inject_thread, nullptr, 0, nullptr);
if (th) CloseHandle(th);
});
// ---------- Footer row inside the outer card --------------------------------------------
// "Campus · Made by Kyle" on the left. Uninstall (red-tinted) + Repair pills on right.
float fRowY = cardsBot + 14.0f;
// Draw "Campus" bold-ish, then a subtle "Made by Kyle" beside it in dimmer white.
const char* brand = "Campus";
const char* byline = "Made by Kyle";
float txtBaseY = fRowY + 10;
fg->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(oPos.x + hpad, txtBaseY),
IM_COL32(255, 255, 255, 230), brand);
float bw = ImGui::CalcTextSize(brand).x;
fg->AddText(g_ui.fontBody, g_ui.fontBody->FontSize,
ImVec2(oPos.x + hpad + bw + 10, txtBaseY),
IM_COL32(255, 255, 255, 130), byline);
const float fbtnW = 92.0f, fbtnH = 34.0f;
ImVec2 repPos(oBR.x - hpad - fbtnW, fRowY + 4);
ImVec2 uninPos(repPos.x - fbtnW - 10, fRowY + 4);
// Repair - default glass pill (fill 5% white, border 30% white).
if (draw_pill_button("Repair", repPos, ImVec2(fbtnW, fbtnH), 0, 0)) {
log_line("Repair: no source configured (placeholder)");
g_ui.status = "Repair: source not configured"; g_ui.statusOk = false;
}
// Uninstall - red warning tint per spec:
// background: rgba(200,50,50,0.15) -> IM_COL32(200,50,50,38)
// border: 1px solid rgba(200,50,50,0.4) -> IM_COL32(200,50,50,102)
if (draw_pill_button("Uninstall", uninPos, ImVec2(fbtnW, fbtnH),
IM_COL32(200, 50, 50, 38),
IM_COL32(200, 50, 50, 102))) {
log_line("Uninstall: not wired up yet (placeholder)");
g_ui.status = "Uninstall: not wired up"; g_ui.statusOk = false;
}
// Outer pink window edge - matches the GDI+ splash's #9B3C96 frame.
fg->AddRect(ImVec2(0.5f, 0.5f), ImVec2(sz.x - 0.5f, sz.y - 0.5f),
IM_COL32(155, 60, 150, 220), 12.0f, 0, 1.5f);
ImGui::PopStyleColor();
ImGui::PopFont();
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
}
// ===============================================================================================
// Window proc - pure WS_POPUP borderless (WM_NCCALCSIZE returns 0)
// ===============================================================================================
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wp, lp)) return true;
switch (msg) {
case WM_NCCALCSIZE: if (wp) return 0; return 0;
case WM_SIZE:
if (g_dev && wp != SIZE_MINIMIZED) {
if (g_rtv) { g_rtv->Release(); g_rtv = nullptr; }
g_sc->ResizeBuffers(0, (UINT)LOWORD(lp), (UINT)HIWORD(lp), DXGI_FORMAT_UNKNOWN, 0);
ID3D11Texture2D* back = nullptr; g_sc->GetBuffer(0, IID_PPV_ARGS(&back));
g_dev->CreateRenderTargetView(back, nullptr, &g_rtv); back->Release();
}
return 0;
case WM_SYSCOMMAND:
if ((wp & 0xfff0) == SC_KEYMENU) return 0;
break;
case WM_DESTROY: PostQuitMessage(0); return 0;
}
return DefWindowProcA(hwnd, msg, wp, lp);
}
// ===============================================================================================
// WinMain
// ===============================================================================================
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int) {
log_line("Campus Launcher started. base=%s", module_dir().c_str());
WNDCLASSEXA wc = { sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, hInst,
LoadIconA(hInst, MAKEINTRESOURCEA(IDI_MAIN_ICON)),
LoadCursor(nullptr, IDC_ARROW),
nullptr, nullptr, "CampusLauncher",
LoadIconA(hInst, MAKEINTRESOURCEA(IDI_MAIN_ICON)) };
RegisterClassExA(&wc);
HWND hwnd = CreateWindowExA(WS_EX_APPWINDOW,
"CampusLauncher", "Campus Launcher", WS_POPUP,
CW_USEDEFAULT, CW_USEDEFAULT, 1020, 700,
nullptr, nullptr, hInst, nullptr);
g_hwnd = hwnd;
{ int c = 2; DwmSetWindowAttribute(hwnd, 33, &c, sizeof(c)); }
if (!create_device(hwnd)) { MessageBoxA(hwnd, "D3D11 init failed", "Campus", MB_ICONERROR); return 1; }
ShowWindow(hwnd, SW_SHOWDEFAULT); UpdateWindow(hwnd);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::StyleColorsDark();
ImGui_ImplWin32_Init(hwnd);
ImGui_ImplDX11_Init(g_dev, g_ctx);
{
const void* fontData; size_t fontSz;
bool haveRubik = find_rcdata(IDR_RUBIK_TTF, fontData, fontSz);
auto add = [&](float px) -> ImFont* {
if (haveRubik) {
ImFontConfig cfg; cfg.FontDataOwnedByAtlas = false;
return io.Fonts->AddFontFromMemoryTTF(const_cast<void*>(fontData),
(int)fontSz, px, &cfg);
} else {
ImFontConfig cfg; cfg.SizePixels = px;
return io.Fonts->AddFontDefault(&cfg);
}
};
g_ui.fontBody = add(15.0f);
g_ui.fontLabel = add(17.0f);
g_ui.fontH = add(28.0f);
g_ui.fontTiny = add(12.5f);
io.Fonts->Build();
}
{
ImGuiStyle& s = ImGui::GetStyle();
s.FrameRounding = 8.0f;
s.GrabRounding = 6.0f;
s.WindowRounding = 0.0f;
s.ItemSpacing = ImVec2(10, 8);
s.FramePadding = ImVec2(10, 6);
}
if (load_embedded_png_texture(g_dev, g_ctx, IDR_ANIMATED_ICON_PNG, g_ui.faceTex))
log_line("Loaded AnimatedIcon (%dx%d) w/ mips", g_ui.faceTex.w, g_ui.faceTex.h);
if (load_embedded_png_texture(g_dev, g_ctx, IDR_BACKGROUND_PNG, g_ui.bgTex))
log_line("Loaded Background (%dx%d) w/ mips", g_ui.bgTex.w, g_ui.bgTex.h);
if (load_embedded_png_texture(g_dev, g_ctx, IDR_CUTOUT_PNG, g_ui.logoTex))
log_line("Loaded Cutout logo (%dx%d) w/ mips", g_ui.logoTex.w, g_ui.logoTex.h);
// Steam + Meta loaded at NATIVE colours. Earlier we made silhouettes and tinted with a
// brand colour, but that collapsed Steam's inner gearwheel + "S" detail into a solid
// purple blob (Meta's infinity is a pure silhouette so it worked fine either way). Native
// colours preserve both icons' inner detail cleanly.
if (load_embedded_png_texture(g_dev, g_ctx, IDR_STEAM_ICON_PNG, g_ui.steamTex))
log_line("Loaded Steam icon (%dx%d) w/ mips", g_ui.steamTex.w, g_ui.steamTex.h);
if (load_embedded_png_texture(g_dev, g_ctx, IDR_META_LOGO_PNG, g_ui.metaTex))
log_line("Loaded Meta logo (%dx%d) w/ mips", g_ui.metaTex.w, g_ui.metaTex.h);
g_ui.status = "Click LAUNCH to find Campus and start the game.";
bool done = false;
while (!done) {
MSG msg;
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg); DispatchMessage(&msg);
if (msg.message == WM_QUIT) done = true;
}
if (done) break;
if (GetForegroundWindow() != hwnd) Sleep(80);
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
draw_ui();
ImGui::Render();
float clear[4] = { 0.05f, 0.02f, 0.07f, 1.0f };
g_ctx->OMSetRenderTargets(1, &g_rtv, nullptr);
g_ctx->ClearRenderTargetView(g_rtv, clear);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
g_sc->Present(1, 0);
}
g_ui.faceTex.release(); g_ui.bgTex.release(); g_ui.logoTex.release();
g_ui.steamTex.release(); g_ui.metaTex.release();
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
destroy_device();
DestroyWindow(hwnd);
UnregisterClassA("CampusLauncher", hInst);
return 0;
}