From e504f88167304d5fa872f0d700a94bdb735b00d3 Mon Sep 17 00:00:00 2001 From: paste Date: Mon, 22 Jun 2026 23:32:54 -0500 Subject: [PATCH] Add account manager, character reordering, and glue Lua console MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three features layered onto the Turtle WoW baseline: - Account manager (AccountLogin.lua / .xml): saved-account list backed by Windows Credential Manager via ClassicAPI. Pass-through to LoginWithSavedAccount; per-account 'last used' timestamp; password field hides itself when the typed name matches a saved entry and a 'Change password' button reveals it on demand. - Character reordering (CharacterSelect.lua / .xml): drag-to-reorder the character list, persisted via ClassicAPI's GetSavedCharacterOrder / SetSavedCharacterOrder. - Glue Lua console (GlueConsole.lua / .xml, new files): in-glue RunScript console reachable from the title and char-select screens. Currently no key opens the console — the backtick binding was removed pending a ClassicAPI EditBox arrow/key script handler (see TODO.md section 96). --- AccountLogin.lua | 431 +++++++++++++--- AccountLogin.xml | 1142 +++++++++++++++++++++++++++++++++++++------ CharacterSelect.lua | 365 ++++++++++++-- CharacterSelect.xml | 84 +++- GlueConsole.lua | 65 +++ GlueConsole.xml | 119 +++++ 6 files changed, 1940 insertions(+), 266 deletions(-) create mode 100644 GlueConsole.lua create mode 100644 GlueConsole.xml diff --git a/AccountLogin.lua b/AccountLogin.lua index 401b4c1..abf53f1 100644 --- a/AccountLogin.lua +++ b/AccountLogin.lua @@ -1,3 +1,254 @@ +-- Autologin: account selection list backed by Windows Credential +-- Manager via ClassicAPI. Passwords live encrypted in the OS +-- credential vault and are never exposed to Lua — `LoginWithSavedAccount` +-- dispatches the login from C so plaintext never crosses the +-- C↔Lua boundary on the way out. +-- +-- `GetSavedAccountName` / `SetSavedAccountName` now holds just the +-- last-used account name (no password, no character map). On +-- `Autologin_Load` we use it to pre-select the same account in the +-- list. The first save after this update overwrites whatever the +-- legacy plaintext-password format left in WTF. + +Autologin_Table = {}; +Autologin_SelectedIdx = nil; +Autologin_CurrentPage = 0; +Autologin_PageSize = 4; +Autologin_LimitReached = false; + +-- When the typed account name matches a saved vault entry we hide the +-- password field (and slide the Login button up) since LoginWithSavedAccount +-- will use the vault credential anyway. PASSWORD_FIELD_FORCED lets the +-- "Change password" button re-reveal the field without changing the name +-- — once true, only an account re-selection or a fresh OnShow resets it. +Autologin_PasswordFieldForced = false; + +local function HasSavedPassword(name) + if ( not name or name == "" ) then return false; end + local accounts = GetSavedAccounts(); + for i = 1, table.getn(accounts) do + -- GetSavedAccounts now returns { {name=..., lastUsed=...}, ... }. + if ( accounts[i].name == name ) then return true; end + end + return false; +end + +-- Best-effort extraction of an account name from whatever's currently +-- in GetSavedAccountName(). Under the new scheme we write just the +-- bare name, but legacy installs have "NAME PASSWORD CHAR;..." — we +-- accept either by taking the first non-space token before any space +-- or semicolon. Returns nil if nothing usable. +local function ParseLastAccount(raw) + if ( not raw or raw == "" ) then return nil; end + local _s, _e, n = string.find(raw, "^([^;%s]+)"); + return n; +end + +function Autologin_Load() + Autologin_Table = {}; + + local lastUsedName = GetSavedAccountName() + -- GetSavedAccounts() now returns { {name=..., lastUsed=epochSeconds}, ... } + -- where lastUsed is the Windows-tracked LastWritten timestamp on + -- the vault entry (auto-refreshed by LoginWithSavedAccount). + local accounts = GetSavedAccounts(); + Autologin_SelectedIdx = nil; + for i = 1, table.getn(accounts) do + local entry = accounts[i]; + table.insert(Autologin_Table, { + name = entry.name, + lastUsed = entry.lastUsed or 0, + }); + if entry.name == lastUsedName then Autologin_SelectedIdx = i end + end + + if ( Autologin_SelectedIdx ) then + Autologin_CurrentPage = + math.floor((Autologin_SelectedIdx - 1) / Autologin_PageSize); + else + Autologin_CurrentPage = 0; + end +end + +-- `name` is the new last-used account, or "" to forget. Always-overwrite +-- semantics mean any leftover plaintext from the legacy format is +-- replaced on the first successful login after this update ships. +function Autologin_Save(name) + SetSavedAccountName(name or ""); +end + +function Autologin_SelectAccount(idx) + local i = Autologin_CurrentPage * Autologin_PageSize + idx; + local r = Autologin_Table[i]; + if ( not r ) then return; end + Autologin_PasswordFieldForced = false; + AccountLoginAccountEdit:SetText(r.name); + -- Clear the password field — we no longer have plaintext to + -- repopulate it with. Empty + a known account name signals + -- "use the saved credential" in Autologin_OnLogin below. + AccountLoginPasswordEdit:SetText(""); + -- Don't rely on SetText → OnTextChanged → Autologin_OnNameUpdate + -- firing — call directly so the visible highlight reliably moves + -- to the clicked row. OnNameUpdate is idempotent if the chain + -- does fire afterward. + Autologin_OnNameUpdate(r.name); +end + +function Autologin_OnNameUpdate(name) + Autologin_SelectedIdx = nil; + for i = 1, table.getn(Autologin_Table) do + if ( Autologin_Table[i].name == name ) then Autologin_SelectedIdx = i; end + end + if ( Autologin_SelectedIdx ) then + Autologin_CurrentPage = math.floor((Autologin_SelectedIdx - 1) / Autologin_PageSize); + end + Autologin_UpdateUI(); + Autologin_UpdatePasswordVisibility(); +end + +-- Show the password edit only when we need it: either the typed name +-- isn't a saved vault entry, or the user clicked "Change password" to +-- force the field open. When hidden, the Login button slides up into +-- the password edit's slot so the form doesn't have a gap. +function Autologin_UpdatePasswordVisibility() + local name = AccountLoginAccountEdit:GetText() or ""; + if ( Autologin_PasswordFieldForced or not HasSavedPassword(name) ) then + AccountLoginPasswordEdit:Show(); + AccountLoginLoginButton:ClearAllPoints(); + AccountLoginLoginButton:SetPoint("TOP", 8, -519); + AutologinChangePasswordButton:Hide(); + else + AccountLoginPasswordEdit:SetText(""); + AccountLoginPasswordEdit:Hide(); + AccountLoginLoginButton:ClearAllPoints(); + AccountLoginLoginButton:SetPoint("TOP", AccountLoginPasswordEdit, "TOP", 0, 0); + AutologinChangePasswordButton:Show(); + end +end + +function Autologin_ChangePassword() + Autologin_PasswordFieldForced = true; + Autologin_UpdatePasswordVisibility(); + AccountLogin_FocusPassword(); +end + +-- Formats `epochSeconds` as a "Last used: " line for the row's +-- second text slot. Falls back to "Never" for entries with no +-- timestamp (theoretically only possible for credentials saved by +-- an older build of this DLL that didn't preserve LastWritten). +local function FormatLastUsed(epochSeconds) + if ( not epochSeconds or epochSeconds == 0 ) then + return "Last used: never"; + end + return "Last used: " .. date("%m/%d/%Y", epochSeconds); +end + +function Autologin_UpdateUI() + local skip = Autologin_CurrentPage * Autologin_PageSize; + for i = 1, Autologin_PageSize do + getglobal("AutologinAccountButton" .. i):UnlockHighlight(); + if ( skip + i > table.getn(Autologin_Table) ) then + getglobal("AutologinAccountButton" .. i):Hide(); + else + local r = Autologin_Table[skip + i]; + getglobal("AutologinAccountButton" .. i):Show(); + getglobal("AutologinAccountButton" .. i .. "ButtonTextName"):SetText(r.name); + getglobal("AutologinAccountButton" .. i .. "ButtonTextPassword"):SetText( + FormatLastUsed(r.lastUsed)); + -- Character autoselect was removed alongside the + -- plaintext store; the row's character line is always + -- blank now. + getglobal("AutologinAccountButton" .. i .. "ButtonTextCharacter"):SetText(""); + + if ( Autologin_SelectedIdx == skip + i ) then + getglobal("AutologinAccountButton" .. i):LockHighlight(); + end + end + end + + -- AutologinSizeWarning was tied to the 128-byte SavedAccount cap + -- for the legacy "name pw char;name pw char;..." format. With + -- only the last-used account name persisted, the cap is + -- effectively unreachable; keep the widget hidden. + getglobal("AutologinSizeWarning"):Hide(); +end + +function Autologin_OnLogin() + local name = AccountLoginAccountEdit:GetText(); + local password = AccountLoginPasswordEdit:GetText(); + + if ( name == nil or name == "" ) then return; end + + if ( password ~= nil and password ~= "" ) then + -- New credentials or overwrite of an existing entry. + -- SaveAccount + LoginWithSavedAccount means the plaintext + -- password lives in this Lua scope only briefly, and is + -- never persisted to SavedVariables. + SaveAccount(name, password); + Autologin_Save(name); + Autologin_Load(); + Autologin_UpdateUI(); + LoginWithSavedAccount(name); + elseif ( HasSavedPassword(name) ) then + -- Selected a saved account and didn't retype: use the + -- vault entry directly. + Autologin_Save(name); + Autologin_Load(); + Autologin_UpdateUI(); + LoginWithSavedAccount(name); + end + -- Empty password + unknown account = no-op; the engine would + -- have shown an error dialog, but skipping the call keeps the + -- user on the login screen without an interrupting popup. +end + +function AutologinAccountButton_OnClick() + Autologin_SelectAccount(this:GetID()); +end + +function AutologinAccountButton_OnDoubleClick() + Autologin_SelectAccount(this:GetID()); + AccountLogin_Login(); +end + +function Autologin_RemoveAccount() + if ( not Autologin_SelectedIdx ) then return; end + + local removed = Autologin_Table[Autologin_SelectedIdx]; + if ( removed ) then + DeleteAccount(removed.name); + -- If the removed account was also the last-used, forget it + -- so next launch doesn't try to pre-select a missing entry. + if ( ParseLastAccount(GetSavedAccountName()) == removed.name ) then + Autologin_Save(""); + end + end + + -- Rebuild from the authoritative list (the vault). + Autologin_Load(); + Autologin_PasswordFieldForced = false; + AccountLoginAccountEdit:SetText(""); + AccountLoginPasswordEdit:SetText(""); + + if ( Autologin_CurrentPage > 0 and Autologin_CurrentPage * Autologin_PageSize > table.getn(Autologin_Table) - 1 ) then + Autologin_CurrentPage = Autologin_CurrentPage - 1; + end + + Autologin_UpdateUI(); +end + +function Autologin_NextPage() + if ( (Autologin_CurrentPage + 1) * Autologin_PageSize > table.getn(Autologin_Table) - 1 ) then return; end + Autologin_CurrentPage = Autologin_CurrentPage + 1; + Autologin_UpdateUI(); +end + +function Autologin_PrevPage() + if ( Autologin_CurrentPage == 0 ) then return; end + Autologin_CurrentPage = Autologin_CurrentPage - 1; + Autologin_UpdateUI(); +end + FADE_IN_TIME = 2; DEFAULT_TOOLTIP_COLOR = {0.8, 0.8, 0.8, 0.09, 0.09, 0.09}; MAX_PIN_LENGTH = 10; @@ -6,11 +257,13 @@ function AccountLogin_OnLoad() this:SetSequence(0); this:SetCamera(0); + TOSFrame.noticeType = "EULA"; + this:RegisterEvent("SHOW_SERVER_ALERT"); this:RegisterEvent("SHOW_SURVEY_NOTIFICATION"); local versionType, buildType, version, internalVersion, date = GetBuildInfo(); - AccountLoginVersion:SetText(format(VERSION_TEMPLATE, versionType, version, internalVersion, buildType, date)); + AccountLoginVersion:SetText(format(TEXT(VERSION_TEMPLATE), versionType, version, internalVersion, buildType, date)); -- Color edit box backdrops local backdropColor = DEFAULT_TOOLTIP_COLOR; @@ -18,15 +271,14 @@ function AccountLogin_OnLoad() AccountLoginAccountEdit:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6]); AccountLoginPasswordEdit:SetBackdropBorderColor(backdropColor[1], backdropColor[2], backdropColor[3]); AccountLoginPasswordEdit:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6]); - VirtualKeypadText:SetBackdropBorderColor(backdropColor[1], backdropColor[2], backdropColor[3]); - VirtualKeypadText:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6]); end function AccountLogin_OnShow() CurrentGlueMusic = "Sound\\Music\\GlueScreenMusic\\wow_main_theme.mp3"; - - AcceptTOS(); - AcceptEULA(); + + -- Try to show the EULA or the TOS + -- AccountLogin_ShowUserAgreements(); + local serverName = GetServerName(); if(serverName) then AccountLoginRealmName:SetText(serverName); @@ -34,15 +286,30 @@ function AccountLogin_OnShow() AccountLoginRealmName:Hide() end - local accountName = GetSavedAccountName(); - AccountLoginAccountEdit:SetText(accountName); - AccountLoginPasswordEdit:SetText(""); + -- Autologin OnShow. Load sets `Autologin_SelectedIdx` from the + -- last-used account name (via GetSavedAccountName), so honor that + -- here instead of unconditionally picking row 1. + Autologin_Load(); + Autologin_PasswordFieldForced = false; + if ( table.getn(Autologin_Table) ~= 0 ) then + local r = Autologin_Table[Autologin_SelectedIdx or 1]; + if ( r ) then + AccountLoginAccountEdit:SetText(r.name); + AccountLoginPasswordEdit:SetText(""); + Autologin_OnNameUpdate(r.name); + end + end + Autologin_UpdateUI(); + Autologin_UpdatePasswordVisibility(); - if ( accountName == "" ) then + if ( table.getn(Autologin_Table) == 0 ) then AccountLogin_FocusAccountName(); - else + elseif ( AccountLoginPasswordEdit:IsVisible() ) then AccountLogin_FocusPassword(); end + -- If the password field is hidden (saved-account autologin), don't + -- grab focus — Enter will still trigger Login via the parent frame's + -- OnKeyDown handler, and the user can click the name field to edit. end function AccountLogin_FocusPassword() @@ -66,7 +333,7 @@ function AccountLogin_OnKeyDown() else AccountLogin_Exit(); end - + elseif ( arg1 == "ENTER" ) then if ( not TOSAccepted() ) then return; @@ -93,44 +360,17 @@ end function AccountLogin_Login() PlaySound("gsLogin"); - DefaultServerLogin(AccountLoginAccountEdit:GetText(), AccountLoginPasswordEdit:GetText()); - AccountLoginPasswordEdit:SetText(""); - - if ( AccountLoginSaveAccountName:GetChecked() ) then - SetSavedAccountName(AccountLoginAccountEdit:GetText()); - else - SetSavedAccountName(""); - end + Autologin_OnLogin(); end -function AccountLogin_Turtle_Armory_Website() +function AccountLogin_ManageAccount() PlaySound("gsLoginNewAccount"); - LaunchURL(TURTLE_ARMORY_WEBSITE); + LaunchURL(AUTH_NO_TIME_URL); end -function AccountLogin_Turtle_Website() +function AccountLogin_LaunchCommunitySite() PlaySound("gsLoginNewAccount"); - LaunchURL(AUTH_TURTLE_WEBSITE); -end - -function AccountLogin_Turtle_Knowledge_Database() - PlaySound("gsLoginNewAccount"); - LaunchURL(TURTLE_KNOWLEDGE_DATABASE_WEBSITE); -end - -function AccountLogin_Turtle_Community_Forum() - PlaySound("gsLoginNewAccount"); - LaunchURL(TURTLE_COMMUNITY_FORUM_WEBSITE); -end - -function AccountLogin_Turtle_Discord() - PlaySound("gsLoginNewAccount"); - LaunchURL(TURTLE_DISCORD_WEBSITE); -end - -function AccountLogin_Turtle_Reddit() - PlaySound("gsLoginNewAccount"); - LaunchURL(TURTLE_REDDIT_WEBSITE); + LaunchURL(COMMUNITY_URL); end function AccountLogin_Credits() @@ -172,12 +412,73 @@ function AccountLogin_SurveyNotificationDone(accepted) AccountLoginUI:Show(); end +function AccountLogin_ShowUserAgreements() + TOSScrollFrame:Hide(); + EULAScrollFrame:Hide(); + ScanningScrollFrame:Hide(); + ContestScrollFrame:Hide(); + TOSText:Hide(); + EULAText:Hide(); + ScanningText:Hide(); + if ( not EULAAccepted() ) then + if ( ShowEULANotice() ) then + TOSNotice:SetText(EULA_NOTICE); + TOSNotice:Show(); + end + AccountLoginUI:Hide(); + TOSFrame.noticeType = "EULA"; + TOSFrameTitle:SetText(EULA_FRAME_TITLE); + TOSFrameHeader:SetWidth(TOSFrameTitle:GetWidth() + 310); + EULAScrollFrame:Show(); + EULAText:Show(); + TOSFrame:Show(); + elseif ( not TOSAccepted() ) then + if ( ShowTOSNotice() ) then + TOSNotice:SetText(TOS_NOTICE); + TOSNotice:Show(); + end + AccountLoginUI:Hide(); + TOSFrame.noticeType = "TOS"; + TOSFrameTitle:SetText(TOS_FRAME_TITLE); + TOSFrameHeader:SetWidth(TOSFrameTitle:GetWidth() + 310); + TOSScrollFrame:Show(); + TOSText:Show(); + TOSFrame:Show(); + elseif ( not ScanningAccepted() and SHOW_SCANNING_AGREEMENT ) then + if ( ShowScanningNotice() ) then + TOSNotice:SetText(SCANNING_NOTICE); + TOSNotice:Show(); + end + AccountLoginUI:Hide(); + TOSFrame.noticeType = "SCAN"; + TOSFrameTitle:SetText(SCAN_FRAME_TITLE); + TOSFrameHeader:SetWidth(TOSFrameTitle:GetWidth() + 310); + ScanningScrollFrame:Show(); + ScanningText:Show(); + TOSFrame:Show(); + elseif ( not ContestAccepted() and SHOW_CONTEST_AGREEMENT ) then + if ( ShowContestNotice() ) then + TOSNotice:SetText(CONTEST_NOTICE); + TOSNotice:Show(); + end + AccountLoginUI:Hide(); + TOSFrame.noticeType = "CONTEST"; + TOSFrameTitle:SetText(CONTEST_FRAME_TITLE); + TOSFrameHeader:SetWidth(TOSFrameTitle:GetWidth() + 310); + ContestScrollFrame:Show(); + ContestText:Show(); + TOSFrame:Show(); + else + AccountLoginUI:Show(); + TOSFrame:Hide(); + end +end + -- Virtual keypad functions -local buttonText = {} function VirtualKeypadFrame_OnEvent(event) if ( event == "PLAYER_ENTER_PIN" ) then for i=1, 10 do - buttonText[i] = _G["arg"..i] + getglobal("VirtualKeypadButton"..i):SetText(getglobal("arg"..i)); end end -- Randomize location to prevent hacking (yeah right) @@ -185,8 +486,8 @@ function VirtualKeypadFrame_OnEvent(event) local yPadding = 10; local xPos = random(xPadding, GlueParent:GetWidth()-VirtualKeypadFrame:GetWidth()-xPadding); local yPos = random(yPadding, GlueParent:GetHeight()-VirtualKeypadFrame:GetHeight()-yPadding); - --VirtualKeypadFrame:SetPoint("TOPLEFT", GlueParent, "TOPLEFT", xPos, -yPos); - + VirtualKeypadFrame:SetPoint("TOPLEFT", GlueParent, "TOPLEFT", xPos, -yPos); + VirtualKeypadFrame:Show(); VirtualKeypad_UpdateButtons(); end @@ -196,31 +497,45 @@ function VirtualKeypadButton_OnClick() if ( not text ) then text = ""; end + VirtualKeypadText:SetText(text.."*"); VirtualKeypadFrame.PIN = VirtualKeypadFrame.PIN..this:GetID(); - VirtualKeypadText:SetText(VirtualKeypadFrame.PIN); VirtualKeypad_UpdateButtons(); end function VirtualKeypadOkayButton_OnClick() - local PIN = VirtualKeypadText:GetText(); + local PIN = VirtualKeypadFrame.PIN; local numNumbers = strlen(PIN); - if numNumbers < 6 then return end local pinNumber = {}; for i=1, MAX_PIN_LENGTH do if ( i <= numNumbers ) then - pinNumber[i] = nil; - for j=1, 10 do - if tonumber(buttonText[j]) == tonumber(strsub(PIN,i,i)) then - pinNumber[i] = j-1; - end - end + pinNumber[i] = strsub(PIN,i,i); else pinNumber[i] = nil; end end - PINEntered(pinNumber[1], pinNumber[2], pinNumber[3], pinNumber[4], pinNumber[5], pinNumber[6], pinNumber[7], pinNumber[8], pinNumber[9], pinNumber[10]); + PINEntered(pinNumber[1] , pinNumber[2], pinNumber[3], pinNumber[4], pinNumber[5], pinNumber[6], pinNumber[7], pinNumber[8], pinNumber[9], pinNumber[10]); VirtualKeypadFrame:Hide(); end function VirtualKeypad_UpdateButtons() + local numNumbers = strlen(VirtualKeypadFrame.PIN); + if ( numNumbers >= 4 and numNumbers <= MAX_PIN_LENGTH ) then + VirtualKeypadOkayButton:Enable(); + else + VirtualKeypadOkayButton:Disable(); + end + if ( numNumbers == 0 ) then + VirtualKeypadBackButton:Disable(); + else + VirtualKeypadBackButton:Enable(); + end + if ( numNumbers >= MAX_PIN_LENGTH ) then + for i=1, MAX_PIN_LENGTH do + getglobal("VirtualKeypadButton"..i):Disable(); + end + else + for i=1, MAX_PIN_LENGTH do + getglobal("VirtualKeypadButton"..i):Enable(); + end + end end diff --git a/AccountLogin.xml b/AccountLogin.xml index 163e8d2..ca1fb26 100644 --- a/AccountLogin.xml +++ b/AccountLogin.xml @@ -1,6 +1,78 @@