mirror of
https://github.com/hyprwm/hyprlock.git
synced 2024-11-16 23:05:58 +01:00
auth: implement a full pam conversation (#205)
* auth: implement a full pam conversation * input-field: fixup failedAttempts and color change Credits to @bvr-yr * pam: set default module to hyprland * input-field: backup previous asset * auth: restart auth in onPasswordCheckTimer * auth: immediately switch to waiting when input was submitted * auth: remove redundant waitingForPamAuth * auth: add inputRequested and reschedule submitInput * auth: clear password buffer and handle submitInput before input is requested * Revert "input-field: backup previous asset" This reverts commit 89702945be6af4aa43f54688ad34a4ccba994a3e. Without the backup we avoid rendering the prompt placeholder for one frame when the failText is not available. Looks better this way. * auth: fallback to su if pam_module not in /etc/pam.d rare occasion where a path check even works on nix * auth: rename inputSubmitted and resubmit callback * auth: detach failText from the conversation * fix rebase mistake * auth: make sure prompt and failText are not reset when restarting auth needed for labels * auth: force update timers when the prompt changes * auth: remove unused stuff
This commit is contained in:
parent
eb1123fa2e
commit
883fbdfe01
11 changed files with 348 additions and 185 deletions
|
@ -49,6 +49,7 @@ void CConfigManager::init() {
|
||||||
m_config.addConfigValue("general:no_fade_in", Hyprlang::INT{0});
|
m_config.addConfigValue("general:no_fade_in", Hyprlang::INT{0});
|
||||||
m_config.addConfigValue("general:no_fade_out", Hyprlang::INT{0});
|
m_config.addConfigValue("general:no_fade_out", Hyprlang::INT{0});
|
||||||
m_config.addConfigValue("general:ignore_empty_input", Hyprlang::INT{0});
|
m_config.addConfigValue("general:ignore_empty_input", Hyprlang::INT{0});
|
||||||
|
m_config.addConfigValue("general:pam_module", Hyprlang::STRING{"hyprlock"});
|
||||||
|
|
||||||
m_config.addSpecialCategory("background", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true});
|
m_config.addSpecialCategory("background", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true});
|
||||||
m_config.addSpecialConfigValue("background", "monitor", Hyprlang::STRING{""});
|
m_config.addSpecialConfigValue("background", "monitor", Hyprlang::STRING{""});
|
||||||
|
|
166
src/core/Auth.cpp
Normal file
166
src/core/Auth.cpp
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
#include "Auth.hpp"
|
||||||
|
#include "hyprlock.hpp"
|
||||||
|
#include "../helpers/Log.hpp"
|
||||||
|
#include "src/config/ConfigManager.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <security/pam_appl.h>
|
||||||
|
#if __has_include(<security/pam_misc.h>)
|
||||||
|
#include <security/pam_misc.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) {
|
||||||
|
const auto CONVERSATIONSTATE = (CAuth::SPamConversationState*)appdata_ptr;
|
||||||
|
struct pam_response* pamReply = (struct pam_response*)calloc(num_msg, sizeof(struct pam_response));
|
||||||
|
bool initialPrompt = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < num_msg; ++i) {
|
||||||
|
switch (msg[i]->msg_style) {
|
||||||
|
case PAM_PROMPT_ECHO_OFF:
|
||||||
|
case PAM_PROMPT_ECHO_ON: {
|
||||||
|
const auto PROMPT = std::string(msg[i]->msg);
|
||||||
|
const auto PROMPTCHANGED = PROMPT != CONVERSATIONSTATE->prompt;
|
||||||
|
Debug::log(LOG, "PAM_PROMPT: {}", PROMPT);
|
||||||
|
|
||||||
|
if (PROMPTCHANGED)
|
||||||
|
g_pHyprlock->enqueueForceUpdateTimers();
|
||||||
|
|
||||||
|
// Some pam configurations ask for the password twice for whatever reason (Fedora su for example)
|
||||||
|
// When the prompt is the same as the last one, I guess our answer can be the same.
|
||||||
|
if (initialPrompt || PROMPTCHANGED) {
|
||||||
|
CONVERSATIONSTATE->prompt = PROMPT;
|
||||||
|
g_pAuth->waitForInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needed for unlocks via SIGUSR1
|
||||||
|
if (g_pHyprlock->m_bTerminate)
|
||||||
|
return PAM_CONV_ERR;
|
||||||
|
|
||||||
|
pamReply[i].resp = strdup(CONVERSATIONSTATE->input.c_str());
|
||||||
|
initialPrompt = false;
|
||||||
|
} break;
|
||||||
|
case PAM_ERROR_MSG: Debug::log(ERR, "PAM: {}", msg[i]->msg); break;
|
||||||
|
case PAM_TEXT_INFO: Debug::log(LOG, "PAM: {}", msg[i]->msg); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*resp = pamReply;
|
||||||
|
return PAM_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
CAuth::CAuth() {
|
||||||
|
static auto* const PPAMMODULE = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:pam_module"));
|
||||||
|
m_sPamModule = *PPAMMODULE;
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(std::filesystem::path("/etc/pam.d/") / m_sPamModule)) {
|
||||||
|
Debug::log(ERR, "Pam module \"{}\" not found! Falling back to \"su\"", m_sPamModule);
|
||||||
|
m_sPamModule = "su";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void passwordCheckTimerCallback(std::shared_ptr<CTimer> self, void* data) {
|
||||||
|
g_pHyprlock->onPasswordCheckTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAuth::start() {
|
||||||
|
std::thread([this]() {
|
||||||
|
resetConversation();
|
||||||
|
auth();
|
||||||
|
|
||||||
|
g_pHyprlock->addTimer(std::chrono::milliseconds(1), passwordCheckTimerCallback, nullptr);
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAuth::auth() {
|
||||||
|
const pam_conv localConv = {conv, (void*)&m_sConversationState};
|
||||||
|
pam_handle_t* handle = NULL;
|
||||||
|
auto uidPassword = getpwuid(getuid());
|
||||||
|
|
||||||
|
int ret = pam_start(m_sPamModule.c_str(), uidPassword->pw_name, &localConv, &handle);
|
||||||
|
|
||||||
|
if (ret != PAM_SUCCESS) {
|
||||||
|
m_sConversationState.success = false;
|
||||||
|
m_sConversationState.failText = "pam_start failed";
|
||||||
|
Debug::log(ERR, "auth: pam_start failed for {}", m_sPamModule);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = pam_authenticate(handle, 0);
|
||||||
|
|
||||||
|
m_sConversationState.waitingForPamAuth = false;
|
||||||
|
|
||||||
|
if (ret != PAM_SUCCESS) {
|
||||||
|
m_sConversationState.success = false;
|
||||||
|
m_sConversationState.failText = ret == PAM_AUTH_ERR ? "Authentication failed" : "pam_authenticate failed";
|
||||||
|
Debug::log(ERR, "auth: {} for {}", m_sConversationState.failText, m_sPamModule);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = pam_end(handle, ret);
|
||||||
|
|
||||||
|
m_sConversationState.success = true;
|
||||||
|
m_sConversationState.failText = "Successfully authenticated";
|
||||||
|
Debug::log(LOG, "auth: authenticated for {}", m_sPamModule);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAuth::didAuthSucceed() {
|
||||||
|
return m_sConversationState.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearing the input must be done from the main thread
|
||||||
|
static void clearInputTimerCallback(std::shared_ptr<CTimer> self, void* data) {
|
||||||
|
g_pHyprlock->clearPasswordBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAuth::waitForInput() {
|
||||||
|
g_pHyprlock->addTimer(std::chrono::milliseconds(1), clearInputTimerCallback, nullptr);
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lk(m_sConversationState.inputMutex);
|
||||||
|
m_bBlockInput = false;
|
||||||
|
m_sConversationState.waitingForPamAuth = false;
|
||||||
|
m_sConversationState.inputRequested = true;
|
||||||
|
m_sConversationState.inputSubmittedCondition.wait(lk, [this] { return !m_sConversationState.inputRequested || g_pHyprlock->m_bTerminate; });
|
||||||
|
m_bBlockInput = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAuth::submitInput(std::string input) {
|
||||||
|
std::unique_lock<std::mutex> lk(m_sConversationState.inputMutex);
|
||||||
|
|
||||||
|
if (!m_sConversationState.inputRequested)
|
||||||
|
Debug::log(ERR, "SubmitInput called, but the auth thread is not waiting for input!");
|
||||||
|
|
||||||
|
m_sConversationState.input = input;
|
||||||
|
m_sConversationState.inputRequested = false;
|
||||||
|
m_sConversationState.waitingForPamAuth = true;
|
||||||
|
m_sConversationState.inputSubmittedCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> CAuth::getLastFailText() {
|
||||||
|
return m_sConversationState.failText.empty() ? std::nullopt : std::optional(m_sConversationState.failText);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> CAuth::getLastPrompt() {
|
||||||
|
return m_sConversationState.prompt.empty() ? std::nullopt : std::optional(m_sConversationState.prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAuth::checkWaiting() {
|
||||||
|
return m_bBlockInput || m_sConversationState.waitingForPamAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAuth::terminate() {
|
||||||
|
m_sConversationState.inputSubmittedCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAuth::resetConversation() {
|
||||||
|
m_sConversationState.input = "";
|
||||||
|
m_sConversationState.waitingForPamAuth = false;
|
||||||
|
m_sConversationState.inputRequested = false;
|
||||||
|
m_sConversationState.success = false;
|
||||||
|
}
|
54
src/core/Auth.hpp
Normal file
54
src/core/Auth.hpp
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
|
class CAuth {
|
||||||
|
public:
|
||||||
|
struct SPamConversationState {
|
||||||
|
std::string input = "";
|
||||||
|
std::string prompt = "";
|
||||||
|
std::string failText = "";
|
||||||
|
|
||||||
|
std::mutex inputMutex;
|
||||||
|
std::condition_variable inputSubmittedCondition;
|
||||||
|
|
||||||
|
bool waitingForPamAuth = false;
|
||||||
|
bool inputRequested = false;
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
CAuth();
|
||||||
|
|
||||||
|
void start();
|
||||||
|
bool auth();
|
||||||
|
bool didAuthSucceed();
|
||||||
|
|
||||||
|
void waitForInput();
|
||||||
|
void submitInput(std::string input);
|
||||||
|
|
||||||
|
std::optional<std::string> getLastFailText();
|
||||||
|
std::optional<std::string> getLastPrompt();
|
||||||
|
|
||||||
|
bool checkWaiting();
|
||||||
|
|
||||||
|
void terminate();
|
||||||
|
|
||||||
|
// Should only be set via the main thread
|
||||||
|
bool m_bDisplayFailText = false;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SPamConversationState m_sConversationState;
|
||||||
|
|
||||||
|
bool m_bBlockInput = true;
|
||||||
|
|
||||||
|
std::string m_sPamModule;
|
||||||
|
|
||||||
|
void resetConversation();
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::unique_ptr<CAuth> g_pAuth;
|
|
@ -1,78 +0,0 @@
|
||||||
#include "Password.hpp"
|
|
||||||
#include "hyprlock.hpp"
|
|
||||||
#include "../helpers/Log.hpp"
|
|
||||||
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <pwd.h>
|
|
||||||
#include <security/pam_appl.h>
|
|
||||||
#if __has_include(<security/pam_misc.h>)
|
|
||||||
#include <security/pam_misc.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
//
|
|
||||||
int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) {
|
|
||||||
const char* pass = static_cast<const char*>(appdata_ptr);
|
|
||||||
struct pam_response* pam_reply = static_cast<struct pam_response*>(calloc(num_msg, sizeof(struct pam_response)));
|
|
||||||
|
|
||||||
for (int i = 0; i < num_msg; ++i) {
|
|
||||||
switch (msg[i]->msg_style) {
|
|
||||||
case PAM_PROMPT_ECHO_OFF:
|
|
||||||
case PAM_PROMPT_ECHO_ON: pam_reply[i].resp = strdup(pass); break;
|
|
||||||
case PAM_ERROR_MSG: Debug::log(ERR, "PAM: {}", msg[i]->msg); break;
|
|
||||||
case PAM_TEXT_INFO: Debug::log(LOG, "PAM: {}", msg[i]->msg); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*resp = pam_reply;
|
|
||||||
return PAM_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void passwordCheckTimerCallback(std::shared_ptr<CTimer> self, void* data) {
|
|
||||||
g_pHyprlock->onPasswordCheckTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<CPassword::SVerificationResult> CPassword::verify(const std::string& pass) {
|
|
||||||
|
|
||||||
std::shared_ptr<CPassword::SVerificationResult> result = std::make_shared<CPassword::SVerificationResult>(false);
|
|
||||||
|
|
||||||
std::thread([this, result, pass]() {
|
|
||||||
auto auth = [&](std::string auth) -> bool {
|
|
||||||
const pam_conv localConv = {conv, (void*)pass.c_str()};
|
|
||||||
pam_handle_t* handle = NULL;
|
|
||||||
auto uidPassword = getpwuid(getuid());
|
|
||||||
|
|
||||||
int ret = pam_start(auth.c_str(), uidPassword->pw_name, &localConv, &handle);
|
|
||||||
|
|
||||||
if (ret != PAM_SUCCESS) {
|
|
||||||
result->success = false;
|
|
||||||
result->failReason = "pam_start failed";
|
|
||||||
Debug::log(ERR, "auth: pam_start failed for {}", auth);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = pam_authenticate(handle, 0);
|
|
||||||
|
|
||||||
if (ret != PAM_SUCCESS) {
|
|
||||||
result->success = false;
|
|
||||||
result->failReason = ret == PAM_AUTH_ERR ? "Authentication failed" : "pam_authenticate failed";
|
|
||||||
Debug::log(ERR, "auth: {} for {}", result->failReason, auth);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = pam_end(handle, ret);
|
|
||||||
|
|
||||||
result->success = true;
|
|
||||||
result->failReason = "Successfully authenticated";
|
|
||||||
Debug::log(LOG, "auth: authenticated for {}", auth);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
result->realized = auth("hyprlock") || auth("su") || true;
|
|
||||||
g_pHyprlock->addTimer(std::chrono::milliseconds(1), passwordCheckTimerCallback, nullptr);
|
|
||||||
}).detach();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
class CPassword {
|
|
||||||
public:
|
|
||||||
struct SVerificationResult {
|
|
||||||
std::atomic<bool> realized = false;
|
|
||||||
bool success = false;
|
|
||||||
std::string failReason = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
std::shared_ptr<SVerificationResult> verify(const std::string& pass);
|
|
||||||
};
|
|
||||||
|
|
||||||
inline std::unique_ptr<CPassword> g_pPassword = std::make_unique<CPassword>();
|
|
|
@ -2,7 +2,7 @@
|
||||||
#include "../helpers/Log.hpp"
|
#include "../helpers/Log.hpp"
|
||||||
#include "../config/ConfigManager.hpp"
|
#include "../config/ConfigManager.hpp"
|
||||||
#include "../renderer/Renderer.hpp"
|
#include "../renderer/Renderer.hpp"
|
||||||
#include "Password.hpp"
|
#include "Auth.hpp"
|
||||||
#include "Egl.hpp"
|
#include "Egl.hpp"
|
||||||
|
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
@ -381,6 +381,9 @@ void CHyprlock::run() {
|
||||||
|
|
||||||
acquireSessionLock();
|
acquireSessionLock();
|
||||||
|
|
||||||
|
g_pAuth = std::make_unique<CAuth>();
|
||||||
|
g_pAuth->start();
|
||||||
|
|
||||||
registerSignalAction(SIGUSR1, handleUnlockSignal, SA_RESTART);
|
registerSignalAction(SIGUSR1, handleUnlockSignal, SA_RESTART);
|
||||||
registerSignalAction(SIGUSR2, handleForceUpdateSignal);
|
registerSignalAction(SIGUSR2, handleForceUpdateSignal);
|
||||||
registerSignalAction(SIGRTMIN, handlePollTerminate);
|
registerSignalAction(SIGRTMIN, handlePollTerminate);
|
||||||
|
@ -533,6 +536,8 @@ void CHyprlock::run() {
|
||||||
|
|
||||||
pthread_kill(pollThr.native_handle(), SIGRTMIN);
|
pthread_kill(pollThr.native_handle(), SIGRTMIN);
|
||||||
|
|
||||||
|
g_pAuth->terminate();
|
||||||
|
|
||||||
// wait for threads to exit cleanly to avoid a coredump
|
// wait for threads to exit cleanly to avoid a coredump
|
||||||
pollThr.join();
|
pollThr.join();
|
||||||
timersThr.join();
|
timersThr.join();
|
||||||
|
@ -737,32 +742,44 @@ static const ext_session_lock_v1_listener sessionLockListener = {
|
||||||
|
|
||||||
// end session_lock
|
// end session_lock
|
||||||
|
|
||||||
|
static void displayFailTextTimerCallback(std::shared_ptr<CTimer> self, void* data) {
|
||||||
|
g_pAuth->m_bDisplayFailText = false;
|
||||||
|
|
||||||
|
for (auto& o : g_pHyprlock->m_vOutputs) {
|
||||||
|
o->sessionLockSurface->render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CHyprlock::onPasswordCheckTimer() {
|
void CHyprlock::onPasswordCheckTimer() {
|
||||||
// check result
|
// check result
|
||||||
if (m_sPasswordState.result->success) {
|
if (g_pAuth->didAuthSucceed()) {
|
||||||
unlock();
|
unlock();
|
||||||
} else {
|
} else {
|
||||||
Debug::log(LOG, "Authentication failed: {}", m_sPasswordState.result->failReason);
|
|
||||||
m_sPasswordState.lastFailReason = m_sPasswordState.result->failReason;
|
|
||||||
m_sPasswordState.passBuffer = "";
|
|
||||||
m_sPasswordState.failedAttempts += 1;
|
|
||||||
Debug::log(LOG, "Failed attempts: {}", m_sPasswordState.failedAttempts);
|
Debug::log(LOG, "Failed attempts: {}", m_sPasswordState.failedAttempts);
|
||||||
|
|
||||||
|
m_sPasswordState.passBuffer = "";
|
||||||
|
m_sPasswordState.failedAttempts += 1;
|
||||||
|
g_pAuth->m_bDisplayFailText = true;
|
||||||
forceUpdateTimers();
|
forceUpdateTimers();
|
||||||
|
|
||||||
|
g_pHyprlock->addTimer(/* controls error message duration */ std::chrono::seconds(1), displayFailTextTimerCallback, nullptr);
|
||||||
|
|
||||||
|
g_pAuth->start();
|
||||||
|
|
||||||
for (auto& o : m_vOutputs) {
|
for (auto& o : m_vOutputs) {
|
||||||
o->sessionLockSurface->render();
|
o->sessionLockSurface->render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_sPasswordState.result.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CHyprlock::passwordCheckWaiting() {
|
void CHyprlock::clearPasswordBuffer() {
|
||||||
return m_sPasswordState.result.get();
|
if (m_sPasswordState.passBuffer.empty())
|
||||||
}
|
return;
|
||||||
|
|
||||||
std::optional<std::string> CHyprlock::passwordLastFailReason() {
|
m_sPasswordState.passBuffer = "";
|
||||||
return m_sPasswordState.lastFailReason;
|
for (auto& o : m_vOutputs) {
|
||||||
|
o->sessionLockSurface->render();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CHyprlock::renderOutput(const std::string& stringPort) {
|
void CHyprlock::renderOutput(const std::string& stringPort) {
|
||||||
|
@ -798,7 +815,7 @@ void CHyprlock::onKey(uint32_t key, bool down) {
|
||||||
else
|
else
|
||||||
std::erase(m_vPressedKeys, key);
|
std::erase(m_vPressedKeys, key);
|
||||||
|
|
||||||
if (m_sPasswordState.result) {
|
if (g_pAuth->checkWaiting()) {
|
||||||
for (auto& o : m_vOutputs) {
|
for (auto& o : m_vOutputs) {
|
||||||
o->sessionLockSurface->render();
|
o->sessionLockSurface->render();
|
||||||
}
|
}
|
||||||
|
@ -826,7 +843,7 @@ void CHyprlock::onKey(uint32_t key, bool down) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_sPasswordState.result = g_pPassword->verify(m_sPasswordState.passBuffer);
|
g_pAuth->submitInput(m_sPasswordState.passBuffer);
|
||||||
} else if (SYM == XKB_KEY_BackSpace) {
|
} else if (SYM == XKB_KEY_BackSpace) {
|
||||||
if (m_sPasswordState.passBuffer.length() > 0) {
|
if (m_sPasswordState.passBuffer.length() > 0) {
|
||||||
// handle utf-8
|
// handle utf-8
|
||||||
|
@ -941,6 +958,11 @@ std::vector<std::shared_ptr<CTimer>> CHyprlock::getTimers() {
|
||||||
return m_vTimers;
|
return m_vTimers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CHyprlock::enqueueForceUpdateTimers() {
|
||||||
|
addTimer(
|
||||||
|
std::chrono::milliseconds(1), [](std::shared_ptr<CTimer> self, void* data) { forceUpdateTimers(); }, nullptr, false);
|
||||||
|
}
|
||||||
|
|
||||||
void CHyprlock::spawnAsync(const std::string& args) {
|
void CHyprlock::spawnAsync(const std::string& args) {
|
||||||
Debug::log(LOG, "Executing (async) {}", args);
|
Debug::log(LOG, "Executing (async) {}", args);
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
#include "Output.hpp"
|
#include "Output.hpp"
|
||||||
#include "CursorShape.hpp"
|
#include "CursorShape.hpp"
|
||||||
#include "Timer.hpp"
|
#include "Timer.hpp"
|
||||||
#include "Password.hpp"
|
#include "Auth.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
@ -40,6 +40,8 @@ class CHyprlock {
|
||||||
std::shared_ptr<CTimer> addTimer(const std::chrono::system_clock::duration& timeout, std::function<void(std::shared_ptr<CTimer> self, void* data)> cb_, void* data,
|
std::shared_ptr<CTimer> addTimer(const std::chrono::system_clock::duration& timeout, std::function<void(std::shared_ptr<CTimer> self, void* data)> cb_, void* data,
|
||||||
bool force = false);
|
bool force = false);
|
||||||
|
|
||||||
|
void enqueueForceUpdateTimers();
|
||||||
|
|
||||||
void onLockLocked();
|
void onLockLocked();
|
||||||
void onLockFinished();
|
void onLockFinished();
|
||||||
|
|
||||||
|
@ -53,6 +55,7 @@ class CHyprlock {
|
||||||
|
|
||||||
void onKey(uint32_t key, bool down);
|
void onKey(uint32_t key, bool down);
|
||||||
void onPasswordCheckTimer();
|
void onPasswordCheckTimer();
|
||||||
|
void clearPasswordBuffer();
|
||||||
bool passwordCheckWaiting();
|
bool passwordCheckWaiting();
|
||||||
std::optional<std::string> passwordLastFailReason();
|
std::optional<std::string> passwordLastFailReason();
|
||||||
|
|
||||||
|
@ -129,10 +132,9 @@ class CHyprlock {
|
||||||
} m_sLockState;
|
} m_sLockState;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
std::string passBuffer = "";
|
std::string passBuffer = "";
|
||||||
std::shared_ptr<CPassword::SVerificationResult> result;
|
size_t failedAttempts = 0;
|
||||||
std::optional<std::string> lastFailReason;
|
bool displayFailText = false;
|
||||||
size_t failedAttempts = 0;
|
|
||||||
} m_sPasswordState;
|
} m_sPasswordState;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|
|
@ -15,7 +15,7 @@ void help() {
|
||||||
int main(int argc, char** argv, char** envp) {
|
int main(int argc, char** argv, char** envp) {
|
||||||
std::string configPath;
|
std::string configPath;
|
||||||
std::string wlDisplay;
|
std::string wlDisplay;
|
||||||
bool immediate = false;
|
bool immediate = false;
|
||||||
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
std::string arg = argv[i];
|
std::string arg = argv[i];
|
||||||
|
@ -32,8 +32,7 @@ int main(int argc, char** argv, char** envp) {
|
||||||
else if (arg == "--display" && i + 1 < argc) {
|
else if (arg == "--display" && i + 1 < argc) {
|
||||||
wlDisplay = argv[i + 1];
|
wlDisplay = argv[i + 1];
|
||||||
i++;
|
i++;
|
||||||
}
|
} else if (arg == "--immediate") {
|
||||||
else if (arg == "--immediate") {
|
|
||||||
immediate = true;
|
immediate = true;
|
||||||
} else if (arg == "--help" || arg == "-h") {
|
} else if (arg == "--help" || arg == "-h") {
|
||||||
help();
|
help();
|
||||||
|
|
|
@ -131,11 +131,17 @@ IWidget::SFormatResult IWidget::formatString(std::string in) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in.contains("$FAIL")) {
|
if (in.contains("$FAIL")) {
|
||||||
const auto FAIL = g_pHyprlock->passwordLastFailReason();
|
const auto FAIL = g_pAuth->getLastFailText();
|
||||||
replaceAll(in, "$FAIL", FAIL.has_value() ? FAIL.value() : "");
|
replaceAll(in, "$FAIL", FAIL.has_value() ? FAIL.value() : "");
|
||||||
result.allowForceUpdate = true;
|
result.allowForceUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (in.contains("$PROMPT")) {
|
||||||
|
const auto PROMPT = g_pAuth->getLastPrompt();
|
||||||
|
replaceAll(in, "$PROMPT", PROMPT.has_value() ? PROMPT.value() : "");
|
||||||
|
result.allowForceUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (in.contains("$ATTEMPTS")) {
|
if (in.contains("$ATTEMPTS")) {
|
||||||
replaceAllAttempts(in);
|
replaceAllAttempts(in);
|
||||||
result.allowForceUpdate = true;
|
result.allowForceUpdate = true;
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
#include "PasswordInputField.hpp"
|
#include "PasswordInputField.hpp"
|
||||||
#include "../Renderer.hpp"
|
#include "../Renderer.hpp"
|
||||||
#include "../../core/hyprlock.hpp"
|
#include "../../core/hyprlock.hpp"
|
||||||
|
#include "src/core/Auth.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
static void replaceAll(std::string& str, const std::string& from, const std::string& to) {
|
||||||
|
if (from.empty())
|
||||||
|
return;
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = str.find(from, pos)) != std::string::npos) {
|
||||||
|
str.replace(pos, from.length(), to);
|
||||||
|
pos += to.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::unordered_map<std::string, std::any>& props, const std::string& output) :
|
CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::unordered_map<std::string, std::any>& props, const std::string& output) :
|
||||||
outputStringPort(output), shadow(this, props, viewport_) {
|
outputStringPort(output), shadow(this, props, viewport_) {
|
||||||
size = std::any_cast<Hyprlang::VEC2>(props.at("size"));
|
size = std::any_cast<Hyprlang::VEC2>(props.at("size"));
|
||||||
|
@ -15,6 +26,7 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u
|
||||||
fadeTimeoutMs = std::any_cast<Hyprlang::INT>(props.at("fade_timeout"));
|
fadeTimeoutMs = std::any_cast<Hyprlang::INT>(props.at("fade_timeout"));
|
||||||
hiddenInputState.enabled = std::any_cast<Hyprlang::INT>(props.at("hide_input"));
|
hiddenInputState.enabled = std::any_cast<Hyprlang::INT>(props.at("hide_input"));
|
||||||
rounding = std::any_cast<Hyprlang::INT>(props.at("rounding"));
|
rounding = std::any_cast<Hyprlang::INT>(props.at("rounding"));
|
||||||
|
configPlaceholderText = std::any_cast<Hyprlang::STRING>(props.at("placeholder_text"));
|
||||||
configFailText = std::any_cast<Hyprlang::STRING>(props.at("fail_text"));
|
configFailText = std::any_cast<Hyprlang::STRING>(props.at("fail_text"));
|
||||||
col.transitionMs = std::any_cast<Hyprlang::INT>(props.at("fail_transition"));
|
col.transitionMs = std::any_cast<Hyprlang::INT>(props.at("fail_transition"));
|
||||||
col.outer = std::any_cast<Hyprlang::INT>(props.at("outer_color"));
|
col.outer = std::any_cast<Hyprlang::INT>(props.at("outer_color"));
|
||||||
|
@ -48,15 +60,17 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u
|
||||||
|
|
||||||
g_pHyprlock->m_bNumLock = col.invertNum;
|
g_pHyprlock->m_bNumLock = col.invertNum;
|
||||||
|
|
||||||
std::string placeholderText = std::any_cast<Hyprlang::STRING>(props.at("placeholder_text"));
|
|
||||||
|
|
||||||
// Render placeholder if either placeholder_text or fail_text are non-empty
|
// Render placeholder if either placeholder_text or fail_text are non-empty
|
||||||
// as placeholder must be rendered to show fail_text
|
// as placeholder must be rendered to show fail_text
|
||||||
if (!placeholderText.empty() || !configFailText.empty()) {
|
if (!configPlaceholderText.empty() || !configFailText.empty()) {
|
||||||
placeholder.resourceID = "placeholder:" + std::to_string((uintptr_t)this);
|
placeholder.currentText = configPlaceholderText;
|
||||||
|
|
||||||
|
replaceAll(placeholder.currentText, "$PROMPT", "");
|
||||||
|
|
||||||
|
placeholder.resourceID = "placeholder:" + placeholder.currentText + std::to_string((uintptr_t)this);
|
||||||
CAsyncResourceGatherer::SPreloadRequest request;
|
CAsyncResourceGatherer::SPreloadRequest request;
|
||||||
request.id = placeholder.resourceID;
|
request.id = placeholder.resourceID;
|
||||||
request.asset = placeholderText;
|
request.asset = placeholder.currentText;
|
||||||
request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT;
|
request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT;
|
||||||
request.props["font_family"] = std::string{"Sans"};
|
request.props["font_family"] = std::string{"Sans"};
|
||||||
request.props["color"] = CColor{1.0 - col.font.r, 1.0 - col.font.g, 1.0 - col.font.b, 0.5};
|
request.props["color"] = CColor{1.0 - col.font.r, 1.0 - col.font.g, 1.0 - col.font.b, 0.5};
|
||||||
|
@ -65,16 +79,6 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void replaceAllFail(std::string& str, const std::string& from, const std::string& to) {
|
|
||||||
if (from.empty())
|
|
||||||
return;
|
|
||||||
size_t pos = 0;
|
|
||||||
while ((pos = str.find(from, pos)) != std::string::npos) {
|
|
||||||
str.replace(pos, from.length(), to);
|
|
||||||
pos += to.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void fadeOutCallback(std::shared_ptr<CTimer> self, void* data) {
|
static void fadeOutCallback(std::shared_ptr<CTimer> self, void* data) {
|
||||||
CPasswordInputField* p = (CPasswordInputField*)data;
|
CPasswordInputField* p = (CPasswordInputField*)data;
|
||||||
|
|
||||||
|
@ -173,23 +177,19 @@ bool CPasswordInputField::draw(const SRenderData& data) {
|
||||||
|
|
||||||
bool forceReload = false;
|
bool forceReload = false;
|
||||||
|
|
||||||
if (passwordLength == 0 && g_pHyprlock->getPasswordFailedAttempts() > failedAttempts)
|
|
||||||
forceReload = true;
|
|
||||||
|
|
||||||
failedAttempts = g_pHyprlock->getPasswordFailedAttempts();
|
|
||||||
passwordLength = g_pHyprlock->getPasswordBufferDisplayLen();
|
passwordLength = g_pHyprlock->getPasswordBufferDisplayLen();
|
||||||
checkWaiting = g_pHyprlock->passwordCheckWaiting();
|
checkWaiting = g_pAuth->checkWaiting();
|
||||||
|
|
||||||
updateFade();
|
updateFade();
|
||||||
updateDots();
|
updateDots();
|
||||||
updateFailTex();
|
updatePlaceholder();
|
||||||
updateColors();
|
updateColors();
|
||||||
updateHiddenInputState();
|
updateHiddenInputState();
|
||||||
|
|
||||||
static auto TIMER = std::chrono::system_clock::now();
|
static auto TIMER = std::chrono::system_clock::now();
|
||||||
|
|
||||||
if (placeholder.failAsset) {
|
if (placeholder.asset) {
|
||||||
const auto TARGETSIZEX = placeholder.failAsset->texture.m_vSize.x + inputFieldBox.h;
|
const auto TARGETSIZEX = placeholder.asset->texture.m_vSize.x + inputFieldBox.h;
|
||||||
|
|
||||||
if (size.x < TARGETSIZEX) {
|
if (size.x < TARGETSIZEX) {
|
||||||
const auto DELTA = std::clamp((int)std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - TIMER).count(), 8000, 20000);
|
const auto DELTA = std::clamp((int)std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - TIMER).count(), 8000, 20000);
|
||||||
|
@ -287,17 +287,10 @@ bool CPasswordInputField::draw(const SRenderData& data) {
|
||||||
if (passwordLength == 0 && !placeholder.resourceID.empty()) {
|
if (passwordLength == 0 && !placeholder.resourceID.empty()) {
|
||||||
SPreloadedAsset* currAsset = nullptr;
|
SPreloadedAsset* currAsset = nullptr;
|
||||||
|
|
||||||
if (!placeholder.failID.empty()) {
|
if (!placeholder.asset)
|
||||||
if (!placeholder.failAsset)
|
placeholder.asset = g_pRenderer->asyncResourceGatherer->getAssetByID(placeholder.resourceID);
|
||||||
placeholder.failAsset = g_pRenderer->asyncResourceGatherer->getAssetByID(placeholder.failID);
|
|
||||||
|
|
||||||
currAsset = placeholder.failAsset;
|
currAsset = placeholder.asset;
|
||||||
} else {
|
|
||||||
if (!placeholder.asset)
|
|
||||||
placeholder.asset = g_pRenderer->asyncResourceGatherer->getAssetByID(placeholder.resourceID);
|
|
||||||
|
|
||||||
currAsset = placeholder.asset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currAsset) {
|
if (currAsset) {
|
||||||
Vector2D pos = outerBox.pos() + outerBox.size() / 2.f;
|
Vector2D pos = outerBox.pos() + outerBox.size() / 2.f;
|
||||||
|
@ -311,41 +304,53 @@ bool CPasswordInputField::draw(const SRenderData& data) {
|
||||||
return dots.currentAmount != passwordLength || fade.animated || col.animated || redrawShadow || data.opacity < 1.0 || forceReload;
|
return dots.currentAmount != passwordLength || fade.animated || col.animated || redrawShadow || data.opacity < 1.0 || forceReload;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CPasswordInputField::updateFailTex() {
|
void CPasswordInputField::updatePlaceholder() {
|
||||||
const auto FAIL = g_pHyprlock->passwordLastFailReason();
|
if (passwordLength != 0) {
|
||||||
|
if (placeholder.asset && /* keep prompt asset cause it is likely to be used again */ placeholder.isFailText) {
|
||||||
if (checkWaiting)
|
std::erase(placeholder.registeredResourceIDs, placeholder.resourceID);
|
||||||
placeholder.canGetNewFail = true;
|
g_pRenderer->asyncResourceGatherer->unloadAsset(placeholder.asset);
|
||||||
|
placeholder.asset = nullptr;
|
||||||
if (passwordLength != 0 || (checkWaiting && passwordLength == 0)) {
|
placeholder.resourceID = "";
|
||||||
if (placeholder.failAsset) {
|
redrawShadow = true;
|
||||||
g_pRenderer->asyncResourceGatherer->unloadAsset(placeholder.failAsset);
|
|
||||||
placeholder.failAsset = nullptr;
|
|
||||||
placeholder.failID = "";
|
|
||||||
redrawShadow = true;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FAIL.has_value() || !placeholder.canGetNewFail)
|
const auto AUTHFEEDBACK = g_pAuth->m_bDisplayFailText ? g_pAuth->getLastFailText().value_or("Ups, no fail text?") : g_pAuth->getLastPrompt().value_or("Ups, no prompt?");
|
||||||
|
|
||||||
|
if (placeholder.lastAuthFeedback == AUTHFEEDBACK && g_pHyprlock->getPasswordFailedAttempts() == placeholder.failedAttempts)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
placeholder.failText = configFailText;
|
placeholder.failedAttempts = g_pHyprlock->getPasswordFailedAttempts();
|
||||||
replaceAllFail(placeholder.failText, "$FAIL", FAIL.value());
|
placeholder.isFailText = g_pAuth->m_bDisplayFailText;
|
||||||
replaceAllFail(placeholder.failText, "$ATTEMPTS", std::to_string(failedAttempts));
|
placeholder.lastAuthFeedback = AUTHFEEDBACK;
|
||||||
|
|
||||||
|
placeholder.asset = nullptr;
|
||||||
|
|
||||||
|
if (placeholder.isFailText) {
|
||||||
|
placeholder.currentText = configFailText;
|
||||||
|
replaceAll(placeholder.currentText, "$FAIL", AUTHFEEDBACK);
|
||||||
|
replaceAll(placeholder.currentText, "$ATTEMPTS", std::to_string(placeholder.failedAttempts));
|
||||||
|
} else {
|
||||||
|
placeholder.currentText = configPlaceholderText;
|
||||||
|
replaceAll(placeholder.currentText, "$PROMPT", AUTHFEEDBACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholder.resourceID = "placeholder:" + placeholder.currentText + std::to_string((uintptr_t)this);
|
||||||
|
if (std::find(placeholder.registeredResourceIDs.begin(), placeholder.registeredResourceIDs.end(), placeholder.resourceID) != placeholder.registeredResourceIDs.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
placeholder.registeredResourceIDs.push_back(placeholder.resourceID);
|
||||||
|
|
||||||
// query
|
// query
|
||||||
CAsyncResourceGatherer::SPreloadRequest request;
|
CAsyncResourceGatherer::SPreloadRequest request;
|
||||||
request.id = "input-error:" + std::to_string((uintptr_t)this) + ",time:" + std::to_string(time(nullptr));
|
request.id = placeholder.resourceID;
|
||||||
placeholder.failID = request.id;
|
request.asset = placeholder.currentText;
|
||||||
request.asset = placeholder.failText;
|
|
||||||
request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT;
|
request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT;
|
||||||
request.props["font_family"] = std::string{"Sans"};
|
request.props["font_family"] = std::string{"Sans"};
|
||||||
request.props["color"] = col.fail;
|
request.props["color"] = (placeholder.isFailText) ? col.fail : col.font;
|
||||||
request.props["font_size"] = (int)size.y / 4;
|
request.props["font_size"] = (int)size.y / 4;
|
||||||
g_pRenderer->asyncResourceGatherer->requestAsyncAssetPreload(request);
|
g_pRenderer->asyncResourceGatherer->requestAsyncAssetPreload(request);
|
||||||
|
|
||||||
placeholder.canGetNewFail = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CPasswordInputField::updateHiddenInputState() {
|
void CPasswordInputField::updateHiddenInputState() {
|
||||||
|
@ -425,7 +430,7 @@ void CPasswordInputField::updateColors() {
|
||||||
col.stateNum = col.invertNum ? !g_pHyprlock->m_bNumLock : g_pHyprlock->m_bNumLock;
|
col.stateNum = col.invertNum ? !g_pHyprlock->m_bNumLock : g_pHyprlock->m_bNumLock;
|
||||||
col.stateCaps = g_pHyprlock->m_bCapsLock;
|
col.stateCaps = g_pHyprlock->m_bCapsLock;
|
||||||
|
|
||||||
if (placeholder.failID.empty()) {
|
if (!placeholder.isFailText || passwordLength > 0 || (passwordLength == 0 && checkWaiting)) {
|
||||||
if (g_pHyprlock->m_bFadeStarted) {
|
if (g_pHyprlock->m_bFadeStarted) {
|
||||||
if (TARGET == col.check)
|
if (TARGET == col.check)
|
||||||
SOURCE = BORDERLESS ? col.inner : col.outer;
|
SOURCE = BORDERLESS ? col.inner : col.outer;
|
||||||
|
|
|
@ -22,7 +22,7 @@ class CPasswordInputField : public IWidget {
|
||||||
private:
|
private:
|
||||||
void updateDots();
|
void updateDots();
|
||||||
void updateFade();
|
void updateFade();
|
||||||
void updateFailTex();
|
void updatePlaceholder();
|
||||||
void updateHiddenInputState();
|
void updateHiddenInputState();
|
||||||
void updateColors();
|
void updateColors();
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ class CPasswordInputField : public IWidget {
|
||||||
bool checkWaiting = false;
|
bool checkWaiting = false;
|
||||||
|
|
||||||
size_t passwordLength = 0;
|
size_t passwordLength = 0;
|
||||||
size_t failedAttempts = 0;
|
|
||||||
|
|
||||||
Vector2D size;
|
Vector2D size;
|
||||||
Vector2D pos;
|
Vector2D pos;
|
||||||
|
@ -39,7 +38,7 @@ class CPasswordInputField : public IWidget {
|
||||||
Vector2D configPos;
|
Vector2D configPos;
|
||||||
Vector2D configSize;
|
Vector2D configSize;
|
||||||
|
|
||||||
std::string halign, valign, configFailText, outputStringPort;
|
std::string halign, valign, configFailText, outputStringPort, configPlaceholderText;
|
||||||
|
|
||||||
int outThick, rounding;
|
int outThick, rounding;
|
||||||
|
|
||||||
|
@ -63,13 +62,18 @@ class CPasswordInputField : public IWidget {
|
||||||
} fade;
|
} fade;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
std::string resourceID = "";
|
std::string resourceID = "";
|
||||||
SPreloadedAsset* asset = nullptr;
|
SPreloadedAsset* asset = nullptr;
|
||||||
|
|
||||||
|
std::string currentText = "";
|
||||||
|
size_t failedAttempts = 0;
|
||||||
|
bool canGetNewText = true;
|
||||||
|
bool isFailText = false;
|
||||||
|
|
||||||
|
std::string lastAuthFeedback;
|
||||||
|
|
||||||
|
std::vector<std::string> registeredResourceIDs;
|
||||||
|
|
||||||
std::string failID = "";
|
|
||||||
SPreloadedAsset* failAsset = nullptr;
|
|
||||||
bool canGetNewFail = true;
|
|
||||||
std::string failText = "";
|
|
||||||
} placeholder;
|
} placeholder;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|
Loading…
Reference in a new issue