mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
- MSAA: conditional 2-att (off) vs 3-att (on) render pass with auto-resolve - MSAA: multisampled color+depth images, query max supported sample count - MSAA: .setMultisample() on all 25+ main-pass pipelines across 17 renderers - MSAA: recreatePipelines() on every sub-renderer for runtime MSAA changes - MSAA: Renderer::setMsaaSamples() orchestrates swapchain+pipeline+ImGui rebuild - MSAA: Anti-Aliasing combo (Off/2x/4x/8x) in Video settings, persisted - Update auth screen assets and terrain fragment shader
924 lines
34 KiB
C++
924 lines
34 KiB
C++
#include "ui/auth_screen.hpp"
|
|
#include "auth/crypto.hpp"
|
|
#include "core/application.hpp"
|
|
#include "core/logger.hpp"
|
|
#include "rendering/renderer.hpp"
|
|
#include "rendering/vk_context.hpp"
|
|
#include "pipeline/asset_manager.hpp"
|
|
#include "audio/music_manager.hpp"
|
|
#include "game/expansion_profile.hpp"
|
|
#include <imgui.h>
|
|
#include <imgui_impl_vulkan.h>
|
|
#include "stb_image.h"
|
|
#include <filesystem>
|
|
#include <sstream>
|
|
#include <fstream>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <algorithm>
|
|
#include <iomanip>
|
|
#include <array>
|
|
#include <random>
|
|
#include <unordered_map>
|
|
|
|
namespace wowee { namespace ui {
|
|
|
|
static std::string trimAscii(std::string s) {
|
|
auto isSpace = [](unsigned char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; };
|
|
size_t b = 0;
|
|
while (b < s.size() && isSpace(static_cast<unsigned char>(s[b]))) ++b;
|
|
size_t e = s.size();
|
|
while (e > b && isSpace(static_cast<unsigned char>(s[e - 1]))) --e;
|
|
return s.substr(b, e - b);
|
|
}
|
|
|
|
static std::string hexEncode(const std::vector<uint8_t>& data) {
|
|
std::ostringstream ss;
|
|
for (uint8_t b : data)
|
|
ss << std::hex << std::setfill('0') << std::setw(2) << (int)b;
|
|
return ss.str();
|
|
}
|
|
|
|
static std::vector<uint8_t> hexDecode(const std::string& hex) {
|
|
std::vector<uint8_t> bytes;
|
|
for (size_t i = 0; i + 1 < hex.size(); i += 2) {
|
|
uint8_t b = static_cast<uint8_t>(std::stoul(hex.substr(i, 2), nullptr, 16));
|
|
bytes.push_back(b);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
AuthScreen::AuthScreen() {
|
|
}
|
|
|
|
std::string AuthScreen::makeServerKey(const std::string& host, int port) {
|
|
std::ostringstream ss;
|
|
ss << host << ":" << port;
|
|
return ss.str();
|
|
}
|
|
|
|
std::string AuthScreen::currentExpansionId() const {
|
|
auto* reg = core::Application::getInstance().getExpansionRegistry();
|
|
if (reg && reg->getActive()) {
|
|
return reg->getActive()->id;
|
|
}
|
|
return "wotlk";
|
|
}
|
|
|
|
void AuthScreen::selectServerProfile(int index) {
|
|
if (index < 0 || index >= static_cast<int>(servers_.size())) {
|
|
selectedServerIndex_ = -1;
|
|
return;
|
|
}
|
|
|
|
selectedServerIndex_ = index;
|
|
const auto& s = servers_[index];
|
|
|
|
std::snprintf(hostname, sizeof(hostname), "%s", s.hostname.c_str());
|
|
hostname[sizeof(hostname) - 1] = '\0';
|
|
port = s.port;
|
|
|
|
std::snprintf(username, sizeof(username), "%s", s.username.c_str());
|
|
username[sizeof(username) - 1] = '\0';
|
|
|
|
savedPasswordHash = s.passwordHash;
|
|
usingStoredHash = !savedPasswordHash.empty();
|
|
if (usingStoredHash) {
|
|
std::snprintf(password, sizeof(password), "%s", PASSWORD_PLACEHOLDER);
|
|
password[sizeof(password) - 1] = '\0';
|
|
} else {
|
|
password[0] = '\0';
|
|
}
|
|
|
|
if (!s.expansionId.empty()) {
|
|
auto* expReg = core::Application::getInstance().getExpansionRegistry();
|
|
if (expReg && expReg->setActive(s.expansionId)) {
|
|
auto& profiles = expReg->getAllProfiles();
|
|
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
|
|
if (profiles[i].id == s.expansionId) { expansionIndex = i; break; }
|
|
}
|
|
core::Application::getInstance().reloadExpansionData();
|
|
}
|
|
}
|
|
}
|
|
|
|
void AuthScreen::upsertCurrentServerProfile(bool includePasswordHash) {
|
|
const std::string hostStr = hostname;
|
|
if (hostStr.empty() || port <= 0) {
|
|
return;
|
|
}
|
|
|
|
const std::string key = makeServerKey(hostStr, port);
|
|
int foundIndex = -1;
|
|
for (int i = 0; i < static_cast<int>(servers_.size()); ++i) {
|
|
if (makeServerKey(servers_[i].hostname, servers_[i].port) == key) {
|
|
foundIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ServerProfile s;
|
|
s.hostname = hostStr;
|
|
s.port = port;
|
|
s.username = username;
|
|
s.expansionId = currentExpansionId();
|
|
if (includePasswordHash && !savedPasswordHash.empty()) {
|
|
s.passwordHash = savedPasswordHash;
|
|
} else if (foundIndex >= 0) {
|
|
// Preserve existing stored hash if we aren't updating it.
|
|
s.passwordHash = servers_[foundIndex].passwordHash;
|
|
}
|
|
|
|
if (foundIndex >= 0) {
|
|
servers_[foundIndex] = std::move(s);
|
|
selectedServerIndex_ = foundIndex;
|
|
} else {
|
|
servers_.push_back(std::move(s));
|
|
selectedServerIndex_ = static_cast<int>(servers_.size()) - 1;
|
|
}
|
|
|
|
// Keep deterministic ordering (and stable combo ordering) across runs.
|
|
std::sort(servers_.begin(), servers_.end(),
|
|
[](const ServerProfile& a, const ServerProfile& b) {
|
|
if (a.hostname != b.hostname) return a.hostname < b.hostname;
|
|
return a.port < b.port;
|
|
});
|
|
|
|
// Fix up index after sort.
|
|
for (int i = 0; i < static_cast<int>(servers_.size()); ++i) {
|
|
if (makeServerKey(servers_[i].hostname, servers_[i].port) == key) {
|
|
selectedServerIndex_ = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|
// Load saved login info on first render
|
|
if (!loginInfoLoaded) {
|
|
loadLoginInfo();
|
|
loginInfoLoaded = true;
|
|
}
|
|
|
|
if (!bgInitAttempted) {
|
|
bgInitAttempted = true;
|
|
loadBackgroundImage();
|
|
}
|
|
if (bgDescriptorSet) {
|
|
ImVec2 screen = ImGui::GetIO().DisplaySize;
|
|
float screenW = screen.x;
|
|
float screenH = screen.y;
|
|
float imgW = static_cast<float>(bgWidth);
|
|
float imgH = static_cast<float>(bgHeight);
|
|
if (imgW > 0.0f && imgH > 0.0f) {
|
|
float screenAspect = screenW / screenH;
|
|
float imgAspect = imgW / imgH;
|
|
ImVec2 uv0(0.0f, 0.0f);
|
|
ImVec2 uv1(1.0f, 1.0f);
|
|
if (imgAspect > screenAspect) {
|
|
float scale = screenAspect / imgAspect;
|
|
float crop = (1.0f - scale) * 0.5f;
|
|
uv0.x = crop;
|
|
uv1.x = 1.0f - crop;
|
|
} else if (imgAspect < screenAspect) {
|
|
float scale = imgAspect / screenAspect;
|
|
float crop = (1.0f - scale) * 0.5f;
|
|
uv0.y = crop;
|
|
uv1.y = 1.0f - crop;
|
|
}
|
|
ImDrawList* bg = ImGui::GetBackgroundDrawList();
|
|
bg->AddImage(reinterpret_cast<ImTextureID>(bgDescriptorSet),
|
|
ImVec2(0, 0), ImVec2(screenW, screenH), uv0, uv1);
|
|
}
|
|
}
|
|
|
|
auto& app = core::Application::getInstance();
|
|
auto* renderer = app.getRenderer();
|
|
if (!musicInitAttempted) {
|
|
musicInitAttempted = true;
|
|
auto* assets = app.getAssetManager();
|
|
if (renderer) {
|
|
auto* music = renderer->getMusicManager();
|
|
if (music && assets && assets->isInitialized() && !music->isInitialized()) {
|
|
music->initialize(assets);
|
|
}
|
|
}
|
|
}
|
|
if (renderer) {
|
|
auto* music = renderer->getMusicManager();
|
|
if (music) {
|
|
if (!loginMusicVolumeAdjusted_) {
|
|
savedMusicVolume_ = music->getVolume();
|
|
int loginVolume = (savedMusicVolume_ * 80) / 100; // reduce auth music by 20%
|
|
if (loginVolume < 0) loginVolume = 0;
|
|
if (loginVolume > 100) loginVolume = 100;
|
|
music->setVolume(loginVolume);
|
|
loginMusicVolumeAdjusted_ = true;
|
|
}
|
|
music->update(ImGui::GetIO().DeltaTime);
|
|
if (!music->isPlaying()) {
|
|
static std::mt19937 rng(std::random_device{}());
|
|
// Tracks in assets/ root
|
|
static const std::array<const char*, 1> kRootTracks = {
|
|
"Raise the Mug, Sound the Warcry.mp3",
|
|
};
|
|
// Tracks in assets/Original Music/
|
|
static const std::array<const char*, 11> kOriginalTracks = {
|
|
"Gold on the Tide in Booty Bay.mp3",
|
|
"Lanterns Over Lordaeron.mp3",
|
|
"Loot the Dogs.mp3",
|
|
"One More Pull.mp3",
|
|
"Roll Need Greed.mp3",
|
|
"RunBackPolka.mp3",
|
|
"The Barrens Has No End.mp3",
|
|
"The Bone Collector.mp3",
|
|
"Wanderwewill.mp3",
|
|
"WHO PULLED_.mp3",
|
|
"You No Take Candle!.mp3",
|
|
};
|
|
|
|
std::vector<std::string> availableTracks;
|
|
auto tryAddTrack = [&](const std::filesystem::path& base, const char* track) {
|
|
std::filesystem::path p = base / track;
|
|
if (std::filesystem::exists(p)) {
|
|
availableTracks.push_back(p.string());
|
|
}
|
|
};
|
|
for (const char* track : kRootTracks) {
|
|
tryAddTrack("assets", track);
|
|
if (availableTracks.empty())
|
|
tryAddTrack(std::filesystem::current_path() / "assets", track);
|
|
}
|
|
for (const char* track : kOriginalTracks) {
|
|
tryAddTrack(std::filesystem::path("assets") / "Original Music", track);
|
|
tryAddTrack(std::filesystem::current_path() / "assets" / "Original Music", track);
|
|
}
|
|
|
|
if (!availableTracks.empty()) {
|
|
std::uniform_int_distribution<size_t> pick(0, availableTracks.size() - 1);
|
|
const std::string& path = availableTracks[pick(rng)];
|
|
music->playFilePath(path, true, 1800.0f);
|
|
LOG_INFO("AuthScreen: Playing login intro track: ", path);
|
|
musicPlaying = music->isPlaying();
|
|
} else {
|
|
LOG_WARNING("AuthScreen: No login intro tracks found in assets/");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
|
|
ImGui::Begin("Authentication", nullptr, ImGuiWindowFlags_NoCollapse);
|
|
|
|
ImGui::Text("Connect to Server");
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Server settings
|
|
ImGui::Text("Server Settings");
|
|
{
|
|
std::string preview;
|
|
if (selectedServerIndex_ >= 0 && selectedServerIndex_ < static_cast<int>(servers_.size())) {
|
|
preview = makeServerKey(servers_[selectedServerIndex_].hostname, servers_[selectedServerIndex_].port);
|
|
} else {
|
|
preview = makeServerKey(hostname, port) + " (custom)";
|
|
}
|
|
|
|
if (ImGui::BeginCombo("Server", preview.c_str())) {
|
|
bool customSelected = (selectedServerIndex_ < 0);
|
|
if (ImGui::Selectable("Custom...", customSelected)) {
|
|
selectedServerIndex_ = -1;
|
|
}
|
|
if (customSelected) ImGui::SetItemDefaultFocus();
|
|
|
|
ImGui::Separator();
|
|
for (int i = 0; i < static_cast<int>(servers_.size()); ++i) {
|
|
std::string label = makeServerKey(servers_[i].hostname, servers_[i].port);
|
|
if (!servers_[i].username.empty()) {
|
|
label += " (" + servers_[i].username + ")";
|
|
}
|
|
bool selected = (selectedServerIndex_ == i);
|
|
if (ImGui::Selectable(label.c_str(), selected)) {
|
|
selectServerProfile(i);
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
}
|
|
|
|
bool hostChanged = ImGui::InputText("Hostname", hostname, sizeof(hostname));
|
|
bool portChanged = ImGui::InputInt("Port", &port);
|
|
if (hostChanged || portChanged) {
|
|
selectedServerIndex_ = -1;
|
|
}
|
|
if (port < 1) port = 1;
|
|
if (port > 65535) port = 65535;
|
|
|
|
// Expansion selector (populated from ExpansionRegistry)
|
|
auto* registry = core::Application::getInstance().getExpansionRegistry();
|
|
if (registry && !registry->getAllProfiles().empty()) {
|
|
auto& profiles = registry->getAllProfiles();
|
|
// Build combo items: "WotLK (3.3.5a)"
|
|
std::string preview;
|
|
if (expansionIndex >= 0 && expansionIndex < static_cast<int>(profiles.size())) {
|
|
preview = profiles[expansionIndex].shortName + " (" + profiles[expansionIndex].versionString() + ")";
|
|
}
|
|
if (ImGui::BeginCombo("Expansion", preview.c_str())) {
|
|
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
|
|
std::string label = profiles[i].shortName + " (" + profiles[i].versionString() + ")";
|
|
bool selected = (expansionIndex == i);
|
|
if (ImGui::Selectable(label.c_str(), selected)) {
|
|
expansionIndex = i;
|
|
registry->setActive(profiles[i].id);
|
|
core::Application::getInstance().reloadExpansionData();
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
} else {
|
|
ImGui::Text("Expansion: WotLK 3.3.5a (default)");
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Credentials
|
|
ImGui::Text("Credentials");
|
|
ImGui::InputText("Username", username, sizeof(username));
|
|
|
|
// Password with visibility toggle
|
|
ImGuiInputTextFlags passwordFlags = showPassword ? 0 : ImGuiInputTextFlags_Password;
|
|
ImGui::InputText("Password", password, sizeof(password), passwordFlags);
|
|
ImGui::SameLine();
|
|
if (ImGui::Checkbox("Show", &showPassword)) {
|
|
// Checkbox state changed
|
|
}
|
|
|
|
// Optional 2FA / PIN field (some servers require this; e.g. Turtle WoW uses Google Authenticator).
|
|
// Keep it visible pre-connect so we can send LOGON_PROOF immediately after the SRP challenge.
|
|
{
|
|
ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password;
|
|
if (authHandler.getState() == auth::AuthState::PIN_REQUIRED) {
|
|
pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
|
|
}
|
|
ImGui::InputText("2FA / PIN", pinCode, sizeof(pinCode), pinFlags);
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(optional)");
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Connection status
|
|
if (!statusMessage.empty()) {
|
|
if (statusIsError) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
|
} else {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
|
|
}
|
|
ImGui::TextWrapped("%s", statusMessage.c_str());
|
|
ImGui::PopStyleColor();
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
// Connect button
|
|
if (authenticating) {
|
|
auto state = authHandler.getState();
|
|
if (state != auth::AuthState::PIN_REQUIRED && state != auth::AuthState::AUTHENTICATOR_REQUIRED) {
|
|
pinAutoSubmitted_ = false;
|
|
authTimer += ImGui::GetIO().DeltaTime;
|
|
|
|
// Show progress with elapsed time
|
|
char progressBuf[128];
|
|
snprintf(progressBuf, sizeof(progressBuf), "Authenticating... (%.0fs)", authTimer);
|
|
ImGui::Text("%s", progressBuf);
|
|
} else {
|
|
ImGui::TextWrapped("This server requires a 2FA / PIN code. Enter it and submit quickly (the server may time out).");
|
|
|
|
// If the user already typed a code before clicking Connect, submit immediately once.
|
|
if (!pinAutoSubmitted_) {
|
|
bool digitsOnly = true;
|
|
size_t len = std::strlen(pinCode);
|
|
for (size_t i = 0; i < len; ++i) {
|
|
if (pinCode[i] < '0' || pinCode[i] > '9') { digitsOnly = false; break; }
|
|
}
|
|
// Auto-submit if the user prefilled a plausible code.
|
|
// PIN-grid: 4-10 digits. Authenticator (TOTP): typically 6 digits.
|
|
if (digitsOnly && ((len >= 4 && len <= 10) || len == 6)) {
|
|
authHandler.submitSecurityCode(pinCode);
|
|
pinCode[0] = '\0';
|
|
pinAutoSubmitted_ = true;
|
|
}
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Submit 2FA/PIN")) {
|
|
authHandler.submitSecurityCode(pinCode);
|
|
// Don't keep the code around longer than needed.
|
|
pinCode[0] = '\0';
|
|
pinAutoSubmitted_ = true;
|
|
}
|
|
}
|
|
|
|
// Check authentication status
|
|
if (state == auth::AuthState::AUTHENTICATED) {
|
|
setStatus("Authentication successful!", false);
|
|
authenticating = false;
|
|
|
|
// Compute and save password hash if user typed a fresh password
|
|
if (!usingStoredHash) {
|
|
std::string upperUser = username;
|
|
std::string upperPass = password;
|
|
std::transform(upperUser.begin(), upperUser.end(), upperUser.begin(), ::toupper);
|
|
std::transform(upperPass.begin(), upperPass.end(), upperPass.begin(), ::toupper);
|
|
std::string combined = upperUser + ":" + upperPass;
|
|
auto hash = auth::Crypto::sha1(combined);
|
|
savedPasswordHash = hexEncode(hash);
|
|
}
|
|
saveLoginInfo(true);
|
|
|
|
// Call success callback
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
}
|
|
} else if (state == auth::AuthState::FAILED) {
|
|
if (!failureReason.empty()) {
|
|
setStatus(failureReason, true);
|
|
} else {
|
|
setStatus("Authentication failed", true);
|
|
}
|
|
authenticating = false;
|
|
} else if (state != auth::AuthState::PIN_REQUIRED && state != auth::AuthState::AUTHENTICATOR_REQUIRED
|
|
&& authTimer >= AUTH_TIMEOUT) {
|
|
setStatus("Connection timed out - server did not respond", true);
|
|
authenticating = false;
|
|
authHandler.disconnect();
|
|
}
|
|
} else {
|
|
if (ImGui::Button("Connect", ImVec2(160, 40))) {
|
|
attemptAuth(authHandler);
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Clear", ImVec2(160, 40))) {
|
|
statusMessage.clear();
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// Info text
|
|
ImGui::TextWrapped("Enter your account credentials to connect to the authentication server.");
|
|
ImGui::TextWrapped("Default port is 3724.");
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void AuthScreen::stopLoginMusic() {
|
|
auto& app = core::Application::getInstance();
|
|
auto* renderer = app.getRenderer();
|
|
if (!renderer) return;
|
|
auto* music = renderer->getMusicManager();
|
|
if (!music) return;
|
|
if (musicPlaying) {
|
|
music->stopMusic(500.0f);
|
|
musicPlaying = false;
|
|
}
|
|
if (loginMusicVolumeAdjusted_) {
|
|
music->setVolume(savedMusicVolume_);
|
|
loginMusicVolumeAdjusted_ = false;
|
|
}
|
|
}
|
|
|
|
void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) {
|
|
// Validate inputs
|
|
if (strlen(username) == 0) {
|
|
setStatus("Username cannot be empty", true);
|
|
return;
|
|
}
|
|
|
|
// Check if using stored hash (password field contains placeholder)
|
|
bool useHash = usingStoredHash && std::strcmp(password, PASSWORD_PLACEHOLDER) == 0;
|
|
|
|
if (!useHash && strlen(password) == 0) {
|
|
setStatus("Password cannot be empty", true);
|
|
return;
|
|
}
|
|
|
|
if (strlen(hostname) == 0) {
|
|
setStatus("Hostname cannot be empty", true);
|
|
return;
|
|
}
|
|
|
|
// Attempt connection
|
|
std::stringstream ss;
|
|
ss << "Connecting to " << hostname << ":" << port << "...";
|
|
setStatus(ss.str(), false);
|
|
|
|
// Wire up failure callback to capture specific error reason
|
|
failureReason.clear();
|
|
authHandler.setOnFailure([this](const std::string& reason) {
|
|
failureReason = reason;
|
|
});
|
|
|
|
// Configure client version from active expansion profile
|
|
auto* reg = core::Application::getInstance().getExpansionRegistry();
|
|
if (reg) {
|
|
auto* profile = reg->getActive();
|
|
if (profile) {
|
|
auth::ClientInfo info;
|
|
info.majorVersion = profile->majorVersion;
|
|
info.minorVersion = profile->minorVersion;
|
|
info.patchVersion = profile->patchVersion;
|
|
info.build = profile->build;
|
|
info.protocolVersion = profile->protocolVersion;
|
|
info.game = profile->game;
|
|
info.platform = profile->platform;
|
|
info.os = profile->os;
|
|
info.locale = profile->locale;
|
|
info.timezone = profile->timezone;
|
|
authHandler.setClientInfo(info);
|
|
}
|
|
}
|
|
|
|
if (authHandler.connect(hostname, static_cast<uint16_t>(port))) {
|
|
authenticating = true;
|
|
authTimer = 0.0f;
|
|
setStatus("Connected, authenticating...", false);
|
|
pinAutoSubmitted_ = false;
|
|
|
|
// Save login info for next session
|
|
saveLoginInfo(false);
|
|
|
|
const std::string pinStr = trimAscii(pinCode);
|
|
|
|
// Send authentication credentials
|
|
if (useHash) {
|
|
auto hashBytes = hexDecode(savedPasswordHash);
|
|
authHandler.authenticateWithHash(username, hashBytes, pinStr);
|
|
} else {
|
|
usingStoredHash = false;
|
|
authHandler.authenticate(username, password, pinStr);
|
|
}
|
|
|
|
// Don't keep the code around longer than needed.
|
|
pinCode[0] = '\0';
|
|
} else {
|
|
std::stringstream errSs;
|
|
errSs << "Failed to connect to " << hostname << ":" << port
|
|
<< " - check that the server is online and the address is correct";
|
|
setStatus(errSs.str(), true);
|
|
}
|
|
}
|
|
|
|
void AuthScreen::setStatus(const std::string& message, bool isError) {
|
|
statusMessage = message;
|
|
statusIsError = isError;
|
|
}
|
|
|
|
std::string AuthScreen::getConfigPath() {
|
|
std::string dir;
|
|
#ifdef _WIN32
|
|
const char* appdata = std::getenv("APPDATA");
|
|
dir = appdata ? std::string(appdata) + "\\wowee" : ".";
|
|
#else
|
|
const char* home = std::getenv("HOME");
|
|
dir = home ? std::string(home) + "/.wowee" : ".";
|
|
#endif
|
|
return dir + "/login.cfg";
|
|
}
|
|
|
|
void AuthScreen::saveLoginInfo(bool includePasswordHash) {
|
|
upsertCurrentServerProfile(includePasswordHash);
|
|
|
|
std::string path = getConfigPath();
|
|
std::filesystem::path dir = std::filesystem::path(path).parent_path();
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(dir, ec);
|
|
|
|
std::ofstream out(path);
|
|
if (!out.is_open()) {
|
|
LOG_WARNING("Could not save login info to ", path);
|
|
return;
|
|
}
|
|
|
|
out << "version=2\n";
|
|
out << "active=" << makeServerKey(hostname, port) << "\n";
|
|
|
|
for (const auto& s : servers_) {
|
|
out << "\n[server " << makeServerKey(s.hostname, s.port) << "]\n";
|
|
out << "username=" << s.username << "\n";
|
|
if (!s.passwordHash.empty()) {
|
|
out << "password_hash=" << s.passwordHash << "\n";
|
|
}
|
|
if (!s.expansionId.empty()) {
|
|
out << "expansion=" << s.expansionId << "\n";
|
|
}
|
|
}
|
|
|
|
LOG_INFO("Login info saved to ", path);
|
|
}
|
|
|
|
void AuthScreen::loadLoginInfo() {
|
|
std::string path = getConfigPath();
|
|
std::ifstream in(path);
|
|
if (!in.is_open()) return;
|
|
|
|
std::string file((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
|
|
// If this looks like the old flat format, migrate it into a single server entry.
|
|
if (file.find("[server ") == std::string::npos) {
|
|
std::unordered_map<std::string, std::string> kv;
|
|
std::istringstream ss(file);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
line = trimAscii(line);
|
|
if (line.empty() || line[0] == '#') continue;
|
|
size_t eq = line.find('=');
|
|
if (eq == std::string::npos) continue;
|
|
kv[trimAscii(line.substr(0, eq))] = trimAscii(line.substr(eq + 1));
|
|
}
|
|
|
|
std::string host = kv["hostname"];
|
|
int p = 3724;
|
|
try { if (!kv["port"].empty()) p = std::stoi(kv["port"]); } catch (...) {}
|
|
if (!host.empty()) {
|
|
ServerProfile s;
|
|
s.hostname = host;
|
|
s.port = p;
|
|
s.username = kv["username"];
|
|
s.passwordHash = kv["password_hash"];
|
|
s.expansionId = kv["expansion"];
|
|
servers_.push_back(std::move(s));
|
|
selectServerProfile(0);
|
|
}
|
|
|
|
LOG_INFO("Login info loaded from ", path, " (migrated v1 -> v2)");
|
|
return;
|
|
}
|
|
|
|
servers_.clear();
|
|
selectedServerIndex_ = -1;
|
|
|
|
std::string activeKey;
|
|
ServerProfile current;
|
|
bool inServer = false;
|
|
|
|
auto flushServer = [&]() {
|
|
if (!inServer) return;
|
|
if (!current.hostname.empty() && current.port > 0) {
|
|
servers_.push_back(current);
|
|
}
|
|
current = ServerProfile{};
|
|
inServer = false;
|
|
};
|
|
|
|
std::istringstream ss(file);
|
|
std::string line;
|
|
while (std::getline(ss, line)) {
|
|
line = trimAscii(line);
|
|
if (line.empty() || line[0] == '#') continue;
|
|
|
|
if (line.front() == '[' && line.back() == ']') {
|
|
flushServer();
|
|
std::string inside = line.substr(1, line.size() - 2);
|
|
inside = trimAscii(inside);
|
|
const std::string prefix = "server ";
|
|
if (inside.rfind(prefix, 0) == 0) {
|
|
std::string key = trimAscii(inside.substr(prefix.size()));
|
|
// Parse host:port (split on last ':', allow [ipv6]:port).
|
|
std::string hostPart = key;
|
|
int portPart = 3724;
|
|
if (!key.empty() && key.front() == '[') {
|
|
auto rb = key.find(']');
|
|
if (rb != std::string::npos) {
|
|
hostPart = key.substr(1, rb - 1);
|
|
auto colon = key.find(':', rb);
|
|
if (colon != std::string::npos) {
|
|
try { portPart = std::stoi(key.substr(colon + 1)); } catch (...) {}
|
|
}
|
|
}
|
|
} else {
|
|
auto colon = key.rfind(':');
|
|
if (colon != std::string::npos) {
|
|
hostPart = key.substr(0, colon);
|
|
try { portPart = std::stoi(key.substr(colon + 1)); } catch (...) {}
|
|
}
|
|
}
|
|
|
|
current.hostname = hostPart;
|
|
current.port = portPart;
|
|
inServer = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
size_t eq = line.find('=');
|
|
if (eq == std::string::npos) continue;
|
|
std::string key = trimAscii(line.substr(0, eq));
|
|
std::string val = trimAscii(line.substr(eq + 1));
|
|
|
|
if (!inServer) {
|
|
if (key == "active") activeKey = val;
|
|
continue;
|
|
}
|
|
|
|
if (key == "username") current.username = val;
|
|
else if (key == "password_hash") current.passwordHash = val;
|
|
else if (key == "expansion") current.expansionId = val;
|
|
}
|
|
flushServer();
|
|
|
|
if (!servers_.empty()) {
|
|
std::sort(servers_.begin(), servers_.end(),
|
|
[](const ServerProfile& a, const ServerProfile& b) {
|
|
if (a.hostname != b.hostname) return a.hostname < b.hostname;
|
|
return a.port < b.port;
|
|
});
|
|
|
|
if (!activeKey.empty()) {
|
|
for (int i = 0; i < static_cast<int>(servers_.size()); ++i) {
|
|
if (makeServerKey(servers_[i].hostname, servers_[i].port) == activeKey) {
|
|
selectServerProfile(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectedServerIndex_ < 0) {
|
|
selectServerProfile(0);
|
|
}
|
|
}
|
|
|
|
LOG_INFO("Login info loaded from ", path);
|
|
}
|
|
|
|
static uint32_t findMemType(VkPhysicalDevice pd, uint32_t filter, VkMemoryPropertyFlags props) {
|
|
VkPhysicalDeviceMemoryProperties mp;
|
|
vkGetPhysicalDeviceMemoryProperties(pd, &mp);
|
|
for (uint32_t i = 0; i < mp.memoryTypeCount; i++) {
|
|
if ((filter & (1 << i)) && (mp.memoryTypes[i].propertyFlags & props) == props) return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool AuthScreen::loadBackgroundImage() {
|
|
auto& app = core::Application::getInstance();
|
|
auto* renderer = app.getRenderer();
|
|
if (!renderer) return false;
|
|
bgVkCtx = renderer->getVkContext();
|
|
if (!bgVkCtx) return false;
|
|
|
|
std::string imgPath = "assets/krayonsignin.png";
|
|
if (!std::filesystem::exists(imgPath))
|
|
imgPath = (std::filesystem::current_path() / imgPath).string();
|
|
|
|
int channels;
|
|
stbi_set_flip_vertically_on_load(false);
|
|
unsigned char* data = stbi_load(imgPath.c_str(), &bgWidth, &bgHeight, &channels, 4);
|
|
if (!data) {
|
|
LOG_WARNING("Auth screen: failed to load background image: ", imgPath);
|
|
return false;
|
|
}
|
|
|
|
VkDevice device = bgVkCtx->getDevice();
|
|
VkPhysicalDevice physDevice = bgVkCtx->getPhysicalDevice();
|
|
VkDeviceSize imageSize = static_cast<VkDeviceSize>(bgWidth) * bgHeight * 4;
|
|
|
|
// Staging buffer
|
|
VkBuffer stagingBuffer;
|
|
VkDeviceMemory stagingMemory;
|
|
{
|
|
VkBufferCreateInfo bufInfo{};
|
|
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
bufInfo.size = imageSize;
|
|
bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
|
bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
vkCreateBuffer(device, &bufInfo, nullptr, &stagingBuffer);
|
|
|
|
VkMemoryRequirements memReqs;
|
|
vkGetBufferMemoryRequirements(device, stagingBuffer, &memReqs);
|
|
VkMemoryAllocateInfo allocInfo{};
|
|
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
allocInfo.allocationSize = memReqs.size;
|
|
allocInfo.memoryTypeIndex = findMemType(physDevice, memReqs.memoryTypeBits,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
|
vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory);
|
|
vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0);
|
|
|
|
void* mapped;
|
|
vkMapMemory(device, stagingMemory, 0, imageSize, 0, &mapped);
|
|
memcpy(mapped, data, imageSize);
|
|
vkUnmapMemory(device, stagingMemory);
|
|
}
|
|
stbi_image_free(data);
|
|
|
|
// Create VkImage
|
|
{
|
|
VkImageCreateInfo imgInfo{};
|
|
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
imgInfo.imageType = VK_IMAGE_TYPE_2D;
|
|
imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
imgInfo.extent = {static_cast<uint32_t>(bgWidth), static_cast<uint32_t>(bgHeight), 1};
|
|
imgInfo.mipLevels = 1;
|
|
imgInfo.arrayLayers = 1;
|
|
imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
imgInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
vkCreateImage(device, &imgInfo, nullptr, &bgImage);
|
|
|
|
VkMemoryRequirements memReqs;
|
|
vkGetImageMemoryRequirements(device, bgImage, &memReqs);
|
|
VkMemoryAllocateInfo allocInfo{};
|
|
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
allocInfo.allocationSize = memReqs.size;
|
|
allocInfo.memoryTypeIndex = findMemType(physDevice, memReqs.memoryTypeBits,
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
vkAllocateMemory(device, &allocInfo, nullptr, &bgMemory);
|
|
vkBindImageMemory(device, bgImage, bgMemory, 0);
|
|
}
|
|
|
|
// Transfer
|
|
bgVkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
|
|
VkImageMemoryBarrier barrier{};
|
|
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.image = bgImage;
|
|
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
barrier.srcAccessMask = 0;
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
|
|
VkBufferImageCopy region{};
|
|
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
|
region.imageExtent = {static_cast<uint32_t>(bgWidth), static_cast<uint32_t>(bgHeight), 1};
|
|
vkCmdCopyBufferToImage(cmd, stagingBuffer, bgImage,
|
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
|
|
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
});
|
|
|
|
vkDestroyBuffer(device, stagingBuffer, nullptr);
|
|
vkFreeMemory(device, stagingMemory, nullptr);
|
|
|
|
// Image view
|
|
{
|
|
VkImageViewCreateInfo viewInfo{};
|
|
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
viewInfo.image = bgImage;
|
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
|
|
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
vkCreateImageView(device, &viewInfo, nullptr, &bgImageView);
|
|
}
|
|
|
|
// Sampler
|
|
{
|
|
VkSamplerCreateInfo samplerInfo{};
|
|
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
|
samplerInfo.magFilter = VK_FILTER_LINEAR;
|
|
samplerInfo.minFilter = VK_FILTER_LINEAR;
|
|
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
vkCreateSampler(device, &samplerInfo, nullptr, &bgSampler);
|
|
}
|
|
|
|
bgDescriptorSet = ImGui_ImplVulkan_AddTexture(bgSampler, bgImageView,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
|
|
LOG_INFO("Auth screen background loaded: ", bgWidth, "x", bgHeight);
|
|
return true;
|
|
}
|
|
|
|
void AuthScreen::destroyBackgroundImage() {
|
|
if (!bgVkCtx) return;
|
|
VkDevice device = bgVkCtx->getDevice();
|
|
vkDeviceWaitIdle(device);
|
|
if (bgDescriptorSet) { ImGui_ImplVulkan_RemoveTexture(bgDescriptorSet); bgDescriptorSet = VK_NULL_HANDLE; }
|
|
if (bgSampler) { vkDestroySampler(device, bgSampler, nullptr); bgSampler = VK_NULL_HANDLE; }
|
|
if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; }
|
|
if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; }
|
|
if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; }
|
|
}
|
|
|
|
}} // namespace wowee::ui
|