Add account manager, character reordering, and glue Lua console

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).
This commit is contained in:
2026-06-22 23:32:54 -05:00
parent 13d77d77c9
commit e504f88167
6 changed files with 1940 additions and 266 deletions
+373 -58
View File
@@ -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: <date>" 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; FADE_IN_TIME = 2;
DEFAULT_TOOLTIP_COLOR = {0.8, 0.8, 0.8, 0.09, 0.09, 0.09}; DEFAULT_TOOLTIP_COLOR = {0.8, 0.8, 0.8, 0.09, 0.09, 0.09};
MAX_PIN_LENGTH = 10; MAX_PIN_LENGTH = 10;
@@ -6,11 +257,13 @@ function AccountLogin_OnLoad()
this:SetSequence(0); this:SetSequence(0);
this:SetCamera(0); this:SetCamera(0);
TOSFrame.noticeType = "EULA";
this:RegisterEvent("SHOW_SERVER_ALERT"); this:RegisterEvent("SHOW_SERVER_ALERT");
this:RegisterEvent("SHOW_SURVEY_NOTIFICATION"); this:RegisterEvent("SHOW_SURVEY_NOTIFICATION");
local versionType, buildType, version, internalVersion, date = GetBuildInfo(); 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 -- Color edit box backdrops
local backdropColor = DEFAULT_TOOLTIP_COLOR; local backdropColor = DEFAULT_TOOLTIP_COLOR;
@@ -18,15 +271,14 @@ function AccountLogin_OnLoad()
AccountLoginAccountEdit:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6]); AccountLoginAccountEdit:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6]);
AccountLoginPasswordEdit:SetBackdropBorderColor(backdropColor[1], backdropColor[2], backdropColor[3]); AccountLoginPasswordEdit:SetBackdropBorderColor(backdropColor[1], backdropColor[2], backdropColor[3]);
AccountLoginPasswordEdit:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6]); 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 end
function AccountLogin_OnShow() function AccountLogin_OnShow()
CurrentGlueMusic = "Sound\\Music\\GlueScreenMusic\\wow_main_theme.mp3"; CurrentGlueMusic = "Sound\\Music\\GlueScreenMusic\\wow_main_theme.mp3";
AcceptTOS(); -- Try to show the EULA or the TOS
AcceptEULA(); -- AccountLogin_ShowUserAgreements();
local serverName = GetServerName(); local serverName = GetServerName();
if(serverName) then if(serverName) then
AccountLoginRealmName:SetText(serverName); AccountLoginRealmName:SetText(serverName);
@@ -34,15 +286,30 @@ function AccountLogin_OnShow()
AccountLoginRealmName:Hide() AccountLoginRealmName:Hide()
end end
local accountName = GetSavedAccountName(); -- Autologin OnShow. Load sets `Autologin_SelectedIdx` from the
AccountLoginAccountEdit:SetText(accountName); -- last-used account name (via GetSavedAccountName), so honor that
AccountLoginPasswordEdit:SetText(""); -- 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(); AccountLogin_FocusAccountName();
else elseif ( AccountLoginPasswordEdit:IsVisible() ) then
AccountLogin_FocusPassword(); AccountLogin_FocusPassword();
end 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 end
function AccountLogin_FocusPassword() function AccountLogin_FocusPassword()
@@ -66,7 +333,7 @@ function AccountLogin_OnKeyDown()
else else
AccountLogin_Exit(); AccountLogin_Exit();
end end
elseif ( arg1 == "ENTER" ) then elseif ( arg1 == "ENTER" ) then
if ( not TOSAccepted() ) then if ( not TOSAccepted() ) then
return; return;
@@ -93,44 +360,17 @@ end
function AccountLogin_Login() function AccountLogin_Login()
PlaySound("gsLogin"); PlaySound("gsLogin");
DefaultServerLogin(AccountLoginAccountEdit:GetText(), AccountLoginPasswordEdit:GetText()); Autologin_OnLogin();
AccountLoginPasswordEdit:SetText("");
if ( AccountLoginSaveAccountName:GetChecked() ) then
SetSavedAccountName(AccountLoginAccountEdit:GetText());
else
SetSavedAccountName("");
end
end end
function AccountLogin_Turtle_Armory_Website() function AccountLogin_ManageAccount()
PlaySound("gsLoginNewAccount"); PlaySound("gsLoginNewAccount");
LaunchURL(TURTLE_ARMORY_WEBSITE); LaunchURL(AUTH_NO_TIME_URL);
end end
function AccountLogin_Turtle_Website() function AccountLogin_LaunchCommunitySite()
PlaySound("gsLoginNewAccount"); PlaySound("gsLoginNewAccount");
LaunchURL(AUTH_TURTLE_WEBSITE); LaunchURL(COMMUNITY_URL);
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);
end end
function AccountLogin_Credits() function AccountLogin_Credits()
@@ -172,12 +412,73 @@ function AccountLogin_SurveyNotificationDone(accepted)
AccountLoginUI:Show(); AccountLoginUI:Show();
end 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 -- Virtual keypad functions
local buttonText = {}
function VirtualKeypadFrame_OnEvent(event) function VirtualKeypadFrame_OnEvent(event)
if ( event == "PLAYER_ENTER_PIN" ) then if ( event == "PLAYER_ENTER_PIN" ) then
for i=1, 10 do for i=1, 10 do
buttonText[i] = _G["arg"..i] getglobal("VirtualKeypadButton"..i):SetText(getglobal("arg"..i));
end end
end end
-- Randomize location to prevent hacking (yeah right) -- Randomize location to prevent hacking (yeah right)
@@ -185,8 +486,8 @@ function VirtualKeypadFrame_OnEvent(event)
local yPadding = 10; local yPadding = 10;
local xPos = random(xPadding, GlueParent:GetWidth()-VirtualKeypadFrame:GetWidth()-xPadding); local xPos = random(xPadding, GlueParent:GetWidth()-VirtualKeypadFrame:GetWidth()-xPadding);
local yPos = random(yPadding, GlueParent:GetHeight()-VirtualKeypadFrame:GetHeight()-yPadding); 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(); VirtualKeypadFrame:Show();
VirtualKeypad_UpdateButtons(); VirtualKeypad_UpdateButtons();
end end
@@ -196,31 +497,45 @@ function VirtualKeypadButton_OnClick()
if ( not text ) then if ( not text ) then
text = ""; text = "";
end end
VirtualKeypadText:SetText(text.."*");
VirtualKeypadFrame.PIN = VirtualKeypadFrame.PIN..this:GetID(); VirtualKeypadFrame.PIN = VirtualKeypadFrame.PIN..this:GetID();
VirtualKeypadText:SetText(VirtualKeypadFrame.PIN);
VirtualKeypad_UpdateButtons(); VirtualKeypad_UpdateButtons();
end end
function VirtualKeypadOkayButton_OnClick() function VirtualKeypadOkayButton_OnClick()
local PIN = VirtualKeypadText:GetText(); local PIN = VirtualKeypadFrame.PIN;
local numNumbers = strlen(PIN); local numNumbers = strlen(PIN);
if numNumbers < 6 then return end
local pinNumber = {}; local pinNumber = {};
for i=1, MAX_PIN_LENGTH do for i=1, MAX_PIN_LENGTH do
if ( i <= numNumbers ) then if ( i <= numNumbers ) then
pinNumber[i] = nil; pinNumber[i] = strsub(PIN,i,i);
for j=1, 10 do
if tonumber(buttonText[j]) == tonumber(strsub(PIN,i,i)) then
pinNumber[i] = j-1;
end
end
else else
pinNumber[i] = nil; pinNumber[i] = nil;
end end
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(); VirtualKeypadFrame:Hide();
end end
function VirtualKeypad_UpdateButtons() 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 end
+1001 -141
View File
File diff suppressed because it is too large Load Diff
+322 -43
View File
@@ -6,6 +6,23 @@ CHARACTER_ROTATION_CONSTANT = 0.6;
MAX_CHARACTERS_DISPLAYED = 10; MAX_CHARACTERS_DISPLAYED = 10;
MAX_CHARACTERS_PER_REALM = 10; MAX_CHARACTERS_PER_REALM = 10;
AUTO_DRAG_TIME = 0.5;
-- Per-account character autoselect was removed alongside the plaintext
-- credential store; the AutologinSaveCharacterButton checkbox in
-- CharacterSelect.xml is now permanently hidden, and the two functions
-- below are no-op wrappers around `EnterWorld` to keep the existing
-- call sites working.
function Autologin_OnCharactersLoad()
if ( AutologinSaveCharacterButton ) then
AutologinSaveCharacterButton:Hide();
end
end
function Autologin_EnterWorld()
EnterWorld();
end
function CharacterSelect_OnLoad() function CharacterSelect_OnLoad()
this:SetSequence(0); this:SetSequence(0);
this:SetCamera(0); this:SetCamera(0);
@@ -14,6 +31,12 @@ function CharacterSelect_OnLoad()
this.selectedIndex = 0; this.selectedIndex = 0;
this.selectLast = 0; this.selectLast = 0;
this.currentModel = ""; this.currentModel = "";
this.translationTable = {};
this.orderChanged = nil;
this.pressDownButton = nil;
this.pressDownTime = 0;
this.draggedIndex = nil;
this.suppressNextClick = nil;
this:RegisterEvent("ADDON_LIST_UPDATE"); this:RegisterEvent("ADDON_LIST_UPDATE");
this:RegisterEvent("CHARACTER_LIST_UPDATE"); this:RegisterEvent("CHARACTER_LIST_UPDATE");
this:RegisterEvent("UPDATE_SELECTED_CHARACTER"); this:RegisterEvent("UPDATE_SELECTED_CHARACTER");
@@ -36,7 +59,7 @@ function CharacterSelect_OnLoad()
local backdropColor = DEFAULT_TOOLTIP_COLOR; local backdropColor = DEFAULT_TOOLTIP_COLOR;
CharacterSelectCharacterFrame:SetBackdropBorderColor(backdropColor[1], backdropColor[2], backdropColor[3]); CharacterSelectCharacterFrame:SetBackdropBorderColor(backdropColor[1], backdropColor[2], backdropColor[3]);
CharacterSelectCharacterFrame:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6], 0.85); CharacterSelectCharacterFrame:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6], 0.85);
end end
function CharacterSelect_OnShow() function CharacterSelect_OnShow()
@@ -49,7 +72,7 @@ function CharacterSelect_OnShow()
local serverType = ""; local serverType = "";
if ( serverName ) then if ( serverName ) then
if( not connected ) then if( not connected ) then
serverName = serverName.."\n("..SERVER_DOWN..")"; serverName = serverName.."\n("..TEXT(SERVER_DOWN)..")";
end end
if ( isPVP ) then if ( isPVP ) then
if ( isRP ) then if ( isRP ) then
@@ -81,7 +104,7 @@ function CharacterSelect_OnShow()
else else
local billingTimeLeft = GetBillingTimeRemaining(); local billingTimeLeft = GetBillingTimeRemaining();
-- Set default text for the payment plan -- Set default text for the payment plan
local billingText = _G["BILLING_TEXT"..paymentPlan]; local billingText = getglobal("BILLING_TEXT"..paymentPlan);
if ( paymentPlan == 1 ) then if ( paymentPlan == 1 ) then
-- Recurring account -- Recurring account
billingTimeLeft = ceil(billingTimeLeft/(60 * 24)); billingTimeLeft = ceil(billingTimeLeft/(60 * 24));
@@ -92,7 +115,7 @@ function CharacterSelect_OnShow()
-- Free account -- Free account
if ( billingTimeLeft < (24 * 60) ) then if ( billingTimeLeft < (24 * 60) ) then
billingText = format(BILLING_FREE_TIME_EXPIRE, billingTimeLeft.." "..GetText("MINUTES_ABBR", nil, billingTimeLeft)); billingText = format(BILLING_FREE_TIME_EXPIRE, billingTimeLeft.." "..GetText("MINUTES_ABBR", nil, billingTimeLeft));
end end
elseif ( paymentPlan == 3 ) then elseif ( paymentPlan == 3 ) then
-- Fixed but not recurring -- Fixed but not recurring
if ( isGameRoom == 1 ) then if ( isGameRoom == 1 ) then
@@ -107,7 +130,7 @@ function CharacterSelect_OnShow()
billingText = BILLING_FIXED_LASTDAY; billingText = BILLING_FIXED_LASTDAY;
else else
billingText = format(billingText, MinutesToTime(billingTimeLeft)); billingText = format(billingText, MinutesToTime(billingTimeLeft));
end end
end end
elseif ( paymentPlan == 4 ) then elseif ( paymentPlan == 4 ) then
-- Usage plan -- Usage plan
@@ -178,14 +201,19 @@ function CharacterSelect_OnEvent()
if ( event == "ADDON_LIST_UPDATE" ) then if ( event == "ADDON_LIST_UPDATE" ) then
UpdateAddonButton(); UpdateAddonButton();
elseif ( event == "CHARACTER_LIST_UPDATE" ) then elseif ( event == "CHARACTER_LIST_UPDATE" ) then
CharacterSelect_RebuildTranslationTable();
CharacterOrder_Apply();
UpdateCharacterList(); UpdateCharacterList();
CharSelectCharacterName:SetText(GetCharacterInfo(this.selectedIndex)); CharSelectCharacterName:SetText(GetCharacterInfo(GetCharIDFromIndex(this.selectedIndex)));
Autologin_OnCharactersLoad();
elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then
-- arg1 is a server-assigned charID; translate to a display index
-- so `selectedIndex` always means "position in the displayed list".
if ( arg1 == 0 ) then if ( arg1 == 0 ) then
CharSelectCharacterName:SetText(""); CharSelectCharacterName:SetText("");
else else
CharSelectCharacterName:SetText(GetCharacterInfo(arg1)); CharSelectCharacterName:SetText(GetCharacterInfo(arg1));
this.selectedIndex = arg1; this.selectedIndex = GetIndexFromCharID(arg1);
end end
UpdateCharacterSelection(); UpdateCharacterSelection();
elseif ( event == "SELECT_LAST_CHARACTER" ) then elseif ( event == "SELECT_LAST_CHARACTER" ) then
@@ -200,9 +228,7 @@ function CharacterSelect_OnEvent()
GlueDialog_Show("SUGGEST_REALM"); GlueDialog_Show("SUGGEST_REALM");
elseif ( event == "FORCE_RENAME_CHARACTER" ) then elseif ( event == "FORCE_RENAME_CHARACTER" ) then
CharacterRenameDialog:Show(); CharacterRenameDialog:Show();
CharacterRenameBackground:SetHeight(16 + CharacterRenameText1:GetHeight() + CharacterRenameText2:GetHeight() + 23 + CharacterRenameEditBox:GetHeight() + 8 + CharacterRenameButton1:GetHeight() + 16); CharacterRenameText1:SetText(getglobal(arg1));
CharacterRenameText1:SetText(_G[arg1]);
end end
end end
@@ -212,44 +238,56 @@ function CharacterSelect_UpdateModel()
end end
function UpdateCharacterSelection() function UpdateCharacterSelection()
-- Drag-aware: during a drag, dim every slot except the one currently
-- holding the dragged character, and override the highlight to track
-- the drag rather than the engine-side "selected" character.
local draggedIndex = CharacterSelect.draggedIndex;
local highlightIndex = draggedIndex or CharacterSelect.selectedIndex;
for i=1, MAX_CHARACTERS_DISPLAYED, 1 do for i=1, MAX_CHARACTERS_DISPLAYED, 1 do
_G["CharSelectCharacterButton"..i]:UnlockHighlight(); local btn = getglobal("CharSelectCharacterButton"..i);
btn:UnlockHighlight();
if ( draggedIndex and i ~= draggedIndex ) then
btn:SetAlpha(0.6);
else
btn:SetAlpha(1);
end
end end
local index = this.selectedIndex; if ( highlightIndex and (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
if ( (index > 0) and (index <= MAX_CHARACTERS_DISPLAYED) )then getglobal("CharSelectCharacterButton"..highlightIndex):LockHighlight();
_G["CharSelectCharacterButton"..index]:LockHighlight();
end end
end end
function UpdateCharacterList() function UpdateCharacterList()
local numChars = GetNumCharacters(); local numChars = GetNumCharacters();
local index = 1; local index = 1;
local coords;
for i=1, numChars, 1 do for i=1, numChars, 1 do
local name, race, class, level, zone, fileString, gender, ghost = GetCharacterInfo(i); local name, race, class, level, zone, fileString, gender, ghost = GetCharacterInfo(GetCharIDFromIndex(i));
if ( gender == 0 ) then
local button = _G["CharSelectCharacterButton"..index]; gender = "MALE";
else
gender = "FEMALE";
end
local button = getglobal("CharSelectCharacterButton"..index);
if ( not name ) then if ( not name ) then
button:SetText("ERROR - Tell Jeremy"); button:SetText("ERROR - Tell Jeremy");
else else
if ( not zone ) then if ( not zone ) then
zone = ""; zone = "";
end end
local classToken = TW_CLASS_TOKEN and TW_CLASS_TOKEN[class];
local classColor if ( classToken and CLASS_COLORS[classToken] ) then
local classToken = TW_CLASS_TOKEN and TW_CLASS_TOKEN[class] class = CLASS_COLORS[classToken] .. class .. "|r";
if classToken and CLASS_COLORS[classToken] then
classColor = CLASS_COLORS[classToken]
class = classColor .. class .. "|r"
end end
_G["CharSelectCharacterButton"..index.."ButtonTextName"]:SetText(name); getglobal("CharSelectCharacterButton"..index.."ButtonTextName"):SetText(name);
if ( ghost ) then if( ghost ) then
_G["CharSelectCharacterButton"..index.."ButtonTextInfo"]:SetText(format(CHARACTER_SELECT_INFO_GHOST, level, class)); getglobal("CharSelectCharacterButton"..index.."ButtonTextInfo"):SetText(format(TEXT(CHARACTER_SELECT_INFO_GHOST), level, class));
else else
_G["CharSelectCharacterButton"..index.."ButtonTextInfo"]:SetText(format(CHARACTER_SELECT_INFO, level, class)); getglobal("CharSelectCharacterButton"..index.."ButtonTextInfo"):SetText(format(TEXT(CHARACTER_SELECT_INFO), level, class));
end end
_G["CharSelectCharacterButton"..index.."ButtonTextLocation"]:SetText(zone); getglobal("CharSelectCharacterButton"..index.."ButtonTextLocation"):SetText(zone);
end end
button:Show(); button:Show();
@@ -268,18 +306,18 @@ function UpdateCharacterList()
end end
CharacterSelect.createIndex = 0; CharacterSelect.createIndex = 0;
CharSelectCreateCharacterButton:Hide(); CharSelectCreateCharacterButton:Hide();
local connected = IsConnectedToServer(); local connected = IsConnectedToServer();
for i=index, MAX_CHARACTERS_DISPLAYED, 1 do for i=index, MAX_CHARACTERS_DISPLAYED, 1 do
local button = _G["CharSelectCharacterButton"..index]; local button = getglobal("CharSelectCharacterButton"..index);
if ( (CharacterSelect.createIndex == 0) and (numChars < MAX_CHARACTERS_PER_REALM) ) then if ( (CharacterSelect.createIndex == 0) and (numChars < MAX_CHARACTERS_PER_REALM) ) then
CharacterSelect.createIndex = index; CharacterSelect.createIndex = index;
if ( connected ) then if ( connected ) then
--If can create characters position and show the create button --If can create characters position and show the create button
CharSelectCreateCharacterButton:SetID(index); CharSelectCreateCharacterButton:SetID(index);
--CharSelectCreateCharacterButton:SetPoint("TOP", button, "TOP", 0, -5); --CharSelectCreateCharacterButton:SetPoint("TOP", button, "TOP", 0, -5);
CharSelectCreateCharacterButton:Show(); CharSelectCreateCharacterButton:Show();
end end
end end
button:Hide(); button:Hide();
@@ -307,6 +345,12 @@ function CharacterSelect_OnChar()
end end
function CharacterSelectButton_OnClick() function CharacterSelectButton_OnClick()
-- A drag just ended on top of this button — eat the click so the
-- drop doesn't also re-select the character at the new slot.
if ( CharacterSelect.suppressNextClick ) then
CharacterSelect.suppressNextClick = nil;
return;
end
local id = this:GetID(); local id = this:GetID();
if ( id ~= CharacterSelect.selectedIndex ) then if ( id ~= CharacterSelect.selectedIndex ) then
CharacterSelect_SelectCharacter(id); CharacterSelect_SelectCharacter(id);
@@ -314,6 +358,10 @@ function CharacterSelectButton_OnClick()
end end
function CharacterSelectButton_OnDoubleClick() function CharacterSelectButton_OnDoubleClick()
if ( CharacterSelect.suppressNextClick ) then
CharacterSelect.suppressNextClick = nil;
return;
end
local id = this:GetID(); local id = this:GetID();
if ( id ~= CharacterSelect.selectedIndex ) then if ( id ~= CharacterSelect.selectedIndex ) then
CharacterSelect_SelectCharacter(id); CharacterSelect_SelectCharacter(id);
@@ -322,10 +370,10 @@ function CharacterSelectButton_OnDoubleClick()
end end
function CharacterSelect_TabResize() function CharacterSelect_TabResize()
local buttonMiddle = _G[this:GetName().."Middle"]; local buttonMiddle = getglobal(this:GetName().."Middle");
local buttonMiddleDisabled = _G[this:GetName().."MiddleDisabled"]; local buttonMiddleDisabled = getglobal(this:GetName().."MiddleDisabled");
local width = this:GetTextWidth() - 8; local width = this:GetTextWidth() - 8;
local leftWidth = _G[this:GetName().."Left"]:GetWidth(); local leftWidth = getglobal(this:GetName().."Left"):GetWidth();
buttonMiddle:SetWidth(width); buttonMiddle:SetWidth(width);
buttonMiddleDisabled:SetWidth(width); buttonMiddleDisabled:SetWidth(width);
this:SetWidth(width + (2 * leftWidth)); this:SetWidth(width + (2 * leftWidth));
@@ -340,26 +388,27 @@ function CharacterSelect_SelectCharacter(id, noCreate)
SetGlueScreen("charcreate"); SetGlueScreen("charcreate");
end end
else else
local name, race, class, level, zone, fileString = GetCharacterInfo(id); local charID = GetCharIDFromIndex(id);
local name, race, class, level, zone, fileString = GetCharacterInfo(charID);
if ( fileString ~= CharacterSelect.currentModel ) then if ( fileString ~= CharacterSelect.currentModel ) then
CharacterSelect.currentModel = fileString; CharacterSelect.currentModel = fileString;
SetBackgroundModel(CharacterSelect, fileString); SetBackgroundModel(CharacterSelect, fileString);
end end
SelectCharacter(id); SelectCharacter(charID);
end end
end end
function CharacterDeleteDialog_OnShow() function CharacterDeleteDialog_OnShow()
local name, race, class, level = GetCharacterInfo(CharacterSelect.selectedIndex); local name, race, class, level = GetCharacterInfo(GetCharIDFromIndex(CharacterSelect.selectedIndex));
CharacterDeleteText1:SetText(format(CONFIRM_CHAR_DELETE, name, level, class)); CharacterDeleteText1:SetText(format(TEXT(CONFIRM_CHAR_DELETE), name, level, class));
CharacterDeleteBackground:SetHeight(16 + CharacterDeleteText1:GetHeight() + CharacterDeleteText2:GetHeight() + 23 + CharacterDeleteEditBox:GetHeight() + 8 + CharacterDeleteButton1:GetHeight() + 16); CharacterDeleteBackground:SetHeight(16 + CharacterDeleteText1:GetHeight() + CharacterDeleteText2:GetHeight() + 23 + CharacterDeleteEditBox:GetHeight() + 8 + CharacterDeleteButton1:GetHeight() + 16);
CharacterDeleteButton1:Disable(); CharacterDeleteButton1:Disable();
end end
function CharacterSelect_EnterWorld() function CharacterSelect_EnterWorld()
PlaySound("gsCharacterSelectionEnterWorld"); PlaySound("gsCharacterSelectionEnterWorld");
EnterWorld(); Autologin_EnterWorld();
end end
function CharacterSelect_Exit() function CharacterSelect_Exit()
@@ -374,7 +423,7 @@ end
function CharacterSelect_TechSupport() function CharacterSelect_TechSupport()
PlaySound("gsCharacterSelectionAcctOptions"); PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(TECH_SUPPORT_URL); LaunchURL(TEXT(TECH_SUPPORT_URL));
end end
function CharacterSelect_Delete() function CharacterSelect_Delete()
@@ -409,6 +458,16 @@ function CharacterSelectFrame_OnUpdate()
CHARACTER_SELECT_ROTATION_START_X = GetCursorPosition(); CHARACTER_SELECT_ROTATION_START_X = GetCursorPosition();
SetCharacterSelectFacing(GetCharacterSelectFacing() + diff); SetCharacterSelectFacing(GetCharacterSelectFacing() + diff);
end end
-- Press-and-hold detector: a character button held past AUTO_DRAG_TIME
-- promotes into drag mode. Released sooner, OnMouseUp clears the
-- button and the click flows normally.
if ( CharacterSelect.pressDownButton ) then
CharacterSelect.pressDownTime = CharacterSelect.pressDownTime + arg1;
if ( CharacterSelect.pressDownTime >= AUTO_DRAG_TIME ) then
CharacterSelectButton_OnDragStart(CharacterSelect.pressDownButton);
end
end
end end
function CharacterSelectRotateRight_OnUpdate() function CharacterSelectRotateRight_OnUpdate()
@@ -425,5 +484,225 @@ end
function CharacterSelect_ManageAccount() function CharacterSelect_ManageAccount()
PlaySound("gsCharacterSelectionAcctOptions"); PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(AUTH_NO_TIME_URL); LaunchURL(TEXT(AUTH_NO_TIME_URL));
end
-- ---------------------------------------------------------------------------
-- Character reordering
-- ---------------------------------------------------------------------------
--
-- `CharacterSelect.translationTable[displayIndex] = charID` maps the slot
-- a button occupies on screen to the server-assigned character ID expected
-- by `GetCharacterInfo` / `SelectCharacter` / `DeleteCharacter` / etc.
-- Default identity table (1->1, 2->2, ...) gets reshuffled by MoveCharacter
-- and restored on every CHARACTER_LIST_UPDATE.
function GetCharIDFromIndex(index)
return CharacterSelect.translationTable[index] or index;
end
function GetIndexFromCharID(charID)
-- Fast path while the table is still identity.
if ( not CharacterSelect.orderChanged ) then
return charID;
end
for index = 1, table.getn(CharacterSelect.translationTable) do
if ( CharacterSelect.translationTable[index] == charID ) then
return index;
end
end
return 0;
end
function CharacterSelect_RebuildTranslationTable()
CharacterSelect.translationTable = {};
CharacterSelect.orderChanged = nil;
local numChars = GetNumCharacters();
for i = 1, numChars do
table.insert(CharacterSelect.translationTable, i);
end
end
function MoveCharacter(originIndex, targetIndex, fromDrag)
CharacterSelect.orderChanged = 1;
local n = table.getn(CharacterSelect.translationTable);
if ( n < 2 ) then return; end
if ( targetIndex < 1 ) then
targetIndex = n;
elseif ( targetIndex > n ) then
targetIndex = 1;
end
if ( originIndex == CharacterSelect.selectedIndex ) then
CharacterSelect.selectedIndex = targetIndex;
elseif ( targetIndex == CharacterSelect.selectedIndex ) then
CharacterSelect.selectedIndex = originIndex;
end
local t = CharacterSelect.translationTable;
t[originIndex], t[targetIndex] = t[targetIndex], t[originIndex];
if ( fromDrag ) then
CharacterSelect.draggedIndex = targetIndex;
end
UpdateCharacterSelection();
UpdateCharacterList();
CharacterOrder_Save();
end
-- ---------------------------------------------------------------------------
-- Drag handlers
-- ---------------------------------------------------------------------------
--
-- 1.12 glue widgets don't expose `RegisterForDrag`, so we synthesize drag
-- mode by watching how long a character button is held: any press past
-- AUTO_DRAG_TIME enters drag mode. The dragged button's OnUpdate then
-- watches the cursor's Y position and calls MoveCharacter when it crosses
-- into another slot. Mouse-up tears everything down.
function CharacterSelectButton_OnMouseDown()
CharacterSelect.pressDownButton = this;
CharacterSelect.pressDownTime = 0;
end
function CharacterSelectButton_OnMouseUp()
if ( CharacterSelect.draggedIndex ) then
CharacterSelectButton_OnDragStop(this);
-- The drag concluded on a button; the engine still dispatches
-- OnClick (and OnDoubleClick) afterwards. Suppress one.
CharacterSelect.suppressNextClick = 1;
end
CharacterSelect.pressDownButton = nil;
end
function CharacterSelectButton_OnDragStart(button)
if ( GetNumCharacters() < 2 ) then return; end
CharacterSelect.pressDownButton = nil;
CharacterSelect.draggedIndex = button:GetID();
-- Cache slot geometry once per drag from the live buttons. Stride
-- accounts for the 13px visual overlap defined in CharacterSelect.xml.
CharacterSelect.dragListTop = CharSelectCharacterButton1:GetTop();
CharacterSelect.dragRowStride = CharSelectCharacterButton1:GetTop() - CharSelectCharacterButton2:GetTop();
if ( not CharacterSelect.dragRowStride or CharacterSelect.dragRowStride <= 0 ) then
-- Fallback if button2 wasn't ready (single character, etc.) —
-- shouldn't reach here because of the >=2 guard above, but be safe.
CharacterSelect.dragRowStride = 57;
end
button:SetScript("OnUpdate", CharacterSelectButton_OnDragUpdate);
UpdateCharacterSelection();
end
function CharacterSelectButton_OnDragUpdate()
if ( not CharacterSelect.draggedIndex ) then
CharacterSelectButton_OnDragStop(this);
return;
end
local _, cursorY = GetCursorPosition();
local top = CharacterSelect.dragListTop;
local stride = CharacterSelect.dragRowStride;
if ( cursorY <= top ) then
local hoverIndex = math.floor((top - cursorY) / stride) + 1;
local hover = getglobal("CharSelectCharacterButton"..hoverIndex);
if ( hover and hover:IsShown() and hoverIndex ~= CharacterSelect.draggedIndex ) then
if ( hoverIndex > CharacterSelect.draggedIndex ) then
MoveCharacter(CharacterSelect.draggedIndex, CharacterSelect.draggedIndex + 1, 1);
else
MoveCharacter(CharacterSelect.draggedIndex, CharacterSelect.draggedIndex - 1, 1);
end
end
end
end
function CharacterSelectButton_OnDragStop(button)
CharacterSelect.pressDownButton = nil;
CharacterSelect.draggedIndex = nil;
if ( button ) then
button:SetScript("OnUpdate", nil);
end
-- draggedIndex is now nil, so UpdateCharacterSelection restores
-- full alpha on every slot and highlights only the selected one.
UpdateCharacterSelection();
end
-- ---------------------------------------------------------------------------
-- Delete / Rename wrappers (selectedIndex is now a display index, so
-- engine calls expecting a charID need translation)
-- ---------------------------------------------------------------------------
function CharacterSelect_DeleteCharacter()
DeleteCharacter(GetCharIDFromIndex(CharacterSelect.selectedIndex));
CharacterDeleteDialog:Hide();
end
function CharacterSelect_RenameCharacter()
if ( RenameCharacter(GetCharIDFromIndex(CharacterSelect.selectedIndex), CharacterRenameEditBox:GetText()) ) then
CharacterRenameDialog:Hide();
end
end
-- ---------------------------------------------------------------------------
-- Persistence — relies on ClassicAPI's glue bindings
-- (GetSavedCharacterOrder / SetSavedCharacterOrder). Both calls are
-- guarded so the drag feature is functional in-session even before the
-- DLL binding ships; persistence kicks in automatically once it does.
-- ---------------------------------------------------------------------------
function CharacterOrder_Load()
if ( type(GetSavedCharacterOrder) ~= "function" ) then return ""; end
local realm = GetServerName() or "";
if ( realm == "" ) then return ""; end
return GetSavedCharacterOrder(realm) or "";
end
function CharacterOrder_Apply()
local saved = CharacterOrder_Load();
if ( not saved or saved == "" ) then return; end
local numChars = GetNumCharacters();
if ( numChars < 2 ) then return; end
-- Build name -> charID lookup against the server's natural order.
-- The translation table is identity at this point (set by
-- RebuildTranslationTable), so charID == display index here.
local nameToID = {};
for charID = 1, numChars do
local name = GetCharacterInfo(charID);
if ( name ) then nameToID[name] = charID; end
end
local newOrder = {};
local claimed = {};
for name in string.gfind(saved, "([^|]+)") do
local charID = nameToID[name];
if ( charID and not claimed[charID] ) then
table.insert(newOrder, charID);
claimed[charID] = 1;
end
end
-- Append any characters that exist on the server but weren't in the
-- saved order (newly created since last save).
for charID = 1, numChars do
if ( not claimed[charID] ) then
table.insert(newOrder, charID);
end
end
-- Only adopt the rebuilt order if it actually differs from identity.
local differs = false;
for i = 1, table.getn(newOrder) do
if ( newOrder[i] ~= i ) then differs = true; break; end
end
if ( differs ) then
CharacterSelect.translationTable = newOrder;
CharacterSelect.orderChanged = 1;
end
end
function CharacterOrder_Save()
if ( type(SetSavedCharacterOrder) ~= "function" ) then return; end
local realm = GetServerName() or "";
if ( realm == "" ) then return; end
local numChars = GetNumCharacters();
local names = {};
for i = 1, numChars do
local name = GetCharacterInfo(GetCharIDFromIndex(i));
if ( name ) then table.insert(names, name); end
end
SetSavedCharacterOrder(realm, table.concat(names, "|"));
end end
+60 -24
View File
@@ -56,6 +56,12 @@
<OnDoubleClick> <OnDoubleClick>
CharacterSelectButton_OnDoubleClick(); CharacterSelectButton_OnDoubleClick();
</OnDoubleClick> </OnDoubleClick>
<OnMouseDown>
CharacterSelectButton_OnMouseDown();
</OnMouseDown>
<OnMouseUp>
CharacterSelectButton_OnMouseUp();
</OnMouseUp>
</Scripts> </Scripts>
<HighlightTexture file="Interface\Glues\CharacterSelect\Glue-CharacterSelect-Highlight" alphaMode="ADD"> <HighlightTexture file="Interface\Glues\CharacterSelect\Glue-CharacterSelect-Highlight" alphaMode="ADD">
<Size> <Size>
@@ -119,16 +125,52 @@
CharacterSelect_EnterWorld(); CharacterSelect_EnterWorld();
</OnClick> </OnClick>
</Scripts> </Scripts>
<NormalText>
<Anchors>
<Anchor point="CENTER">
<Offset>
<AbsDimension x="-1" y="3"/>
</Offset>
</Anchor>
</Anchors>
</NormalText>
</Button> </Button>
<CheckButton name="AutologinSaveCharacterButton">
<Size>
<AbsDimension x="20" y="20"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="CharSelectEnterWorldButton" relativePoint="RIGHT"/>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString inherits="GlueFontNormalSmall" text="Auto-login this character">
<Anchors>
<Anchor point="LEFT">
<Offset>
<AbsDimension x="24" y="0"/>
</Offset>
</Anchor>
</Anchors>
<FontHeight>
<AbsValue val="10"/>
</FontHeight>
<Shadow>
<Offset>
<AbsDimension x="1" y="-1"/>
</Offset>
<Color r="0" g="0" b="0"/>
</Shadow>
<Color r="1.0" g="0.78" b="0"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick>
if ( this:GetChecked() ) then
PlaySound("igMainMenuOptionCheckBoxOff");
else
PlaySound("igMainMenuOptionCheckBoxOn");
end
</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
<HighlightTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/>
<DisabledCheckedTexture file="Interface\Buttons\UI-CheckBox-Check-Disabled"/>
</CheckButton>
<Button name="CharacterSelectRotateLeft"> <Button name="CharacterSelectRotateLeft">
<Size> <Size>
<AbsDimension x="50" y="50"/> <AbsDimension x="50" y="50"/>
@@ -601,7 +643,7 @@
<Anchors> <Anchors>
<Anchor point="LEFT"> <Anchor point="LEFT">
<Offset> <Offset>
<AbsDimension x="12" y="4"/> <AbsDimension x="12" y="10"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>
@@ -609,7 +651,7 @@
</Layer> </Layer>
</Layers> </Layers>
<Frames> <Frames>
<Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM"> <Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY">
<Anchors> <Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterDeleteBackground" relativePoint="BOTTOM"> <Anchor point="BOTTOMRIGHT" relativeTo="CharacterDeleteBackground" relativePoint="BOTTOM">
<Offset> <Offset>
@@ -619,8 +661,7 @@
</Anchors> </Anchors>
<Scripts> <Scripts>
<OnClick> <OnClick>
DeleteCharacter(CharacterSelect.selectedIndex); CharacterSelect_DeleteCharacter();
CharacterDeleteDialog:Hide();
PlaySound("gsTitleOptionOK"); PlaySound("gsTitleOptionOK");
</OnClick> </OnClick>
</Scripts> </Scripts>
@@ -690,11 +731,10 @@
else else
CharacterDeleteButton1:Disable(); CharacterDeleteButton1:Disable();
end end
</OnTextChanged> </OnTextChanged>
<OnEnterPressed> <OnEnterPressed>
if ( CharacterDeleteButton1:IsEnabled() == 1 ) then if ( CharacterDeleteButton1:IsEnabled() == 1 ) then
DeleteCharacter(CharacterSelect.selectedIndex); CharacterSelect_DeleteCharacter();
CharacterDeleteDialog:Hide();
end end
</OnEnterPressed> </OnEnterPressed>
<OnEscapePressed> <OnEscapePressed>
@@ -771,7 +811,7 @@
<Anchors> <Anchors>
<Anchor point="LEFT"> <Anchor point="LEFT">
<Offset> <Offset>
<AbsDimension x="12" y="4"/> <AbsDimension x="12" y="10"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>
@@ -779,7 +819,7 @@
</Layer> </Layer>
</Layers> </Layers>
<Frames> <Frames>
<Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM"> <Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY">
<Anchors> <Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterRenameBackground" relativePoint="BOTTOM"> <Anchor point="BOTTOMRIGHT" relativeTo="CharacterRenameBackground" relativePoint="BOTTOM">
<Offset> <Offset>
@@ -789,9 +829,7 @@
</Anchors> </Anchors>
<Scripts> <Scripts>
<OnClick> <OnClick>
if ( RenameCharacter(CharacterSelect.selectedIndex, CharacterRenameEditBox:GetText()) ) then CharacterSelect_RenameCharacter();
CharacterRenameDialog:Hide();
end
</OnClick> </OnClick>
</Scripts> </Scripts>
</Button> </Button>
@@ -854,9 +892,7 @@
</Layers> </Layers>
<Scripts> <Scripts>
<OnEnterPressed> <OnEnterPressed>
if ( RenameCharacter(CharacterSelect.selectedIndex, CharacterRenameEditBox:GetText()) ) then CharacterSelect_RenameCharacter();
CharacterRenameDialog:Hide();
end
</OnEnterPressed> </OnEnterPressed>
<OnEscapePressed> <OnEscapePressed>
CharacterRenameDialog:Hide(); CharacterRenameDialog:Hide();
+65
View File
@@ -0,0 +1,65 @@
GLUE_CONSOLE_MAX_LINES = 200;
function GlueConsole_OnLoad()
GlueConsoleOutput:SetFading(false);
GlueConsoleOutput:SetMaxLines(GLUE_CONSOLE_MAX_LINES);
GlueConsoleOutput:SetJustifyH("LEFT");
-- ChatFrameBackground is a white pixel; tint to dark + faint border.
GlueConsoleInput:SetBackdropColor(0, 0, 0, 0.7);
GlueConsoleInput:SetBackdropBorderColor(0.4, 0.4, 0.4, 1);
-- Route print() output into the console so RunScript("print(...)") is visible.
local original_print = print;
print = function(...)
local parts = {};
for i = 1, arg.n do
parts[i] = tostring(arg[i]);
end
GlueConsole_Print(table.concat(parts, " "));
if ( original_print ) then
original_print(unpack(arg));
end
end
end
function GlueConsole_Print(msg)
if ( msg == nil ) then msg = "nil"; end
GlueConsoleOutput:AddMessage(tostring(msg));
end
function GlueConsole_Toggle()
if ( GlueConsoleFrame:IsVisible() ) then
GlueConsoleInput:ClearFocus();
GlueConsoleFrame:Hide();
else
GlueConsoleFrame:Show();
GlueConsoleFrame:Raise();
GlueConsoleInput:SetFocus();
end
end
-- Returns true if a backtick was found in the EditBox text. Strips it and toggles the console.
-- Used as an OnTextChanged interceptor so backtick still toggles the console while an EditBox has focus
-- (the parent frame's OnKeyDown doesn't fire when an EditBox has consumed the keystroke).
function GlueConsole_StripBacktickAndToggle(editBox)
local text = editBox:GetText();
if ( text == nil ) then return false; end
if ( string.find(text, "`", 1, true) == nil ) then return false; end
editBox:SetText((string.gsub(text, "`", "")));
GlueConsole_Toggle();
return true;
end
function GlueConsole_Submit()
local text = GlueConsoleInput:GetText();
GlueConsoleInput:SetText("");
if ( text == nil or text == "" ) then return; end
GlueConsole_Print("> " .. text);
if ( RunScript ) then
RunScript(text);
else
GlueConsole_Print("|cffff4444RunScript is not available in this context|r");
end
end
+119
View File
@@ -0,0 +1,119 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="GlueConsole.lua"/>
<Frame name="GlueConsoleFrame" parent="GlueParent" frameStrata="FULLSCREEN_DIALOG" toplevel="true" hidden="true" movable="true">
<Size>
<AbsDimension x="600" y="320"/>
</Size>
<Anchors>
<Anchor point="TOP">
<Offset>
<AbsDimension x="0" y="-20"/>
</Offset>
</Anchor>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="32"/>
</TileSize>
<EdgeSize>
<AbsValue val="32"/>
</EdgeSize>
</Backdrop>
<Layers>
<Layer level="OVERLAY">
<FontString name="GlueConsoleTitle" inherits="GlueFontNormalSmall" text="Lua Console (` to close, Enter to run)">
<Anchors>
<Anchor point="TOP">
<Offset>
<AbsDimension x="0" y="-10"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="GlueConsoleOutputBg">
<Size>
<AbsDimension x="568" y="240"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="16" y="-28"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture>
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollingMessageFrame name="GlueConsoleOutput">
<Size>
<AbsDimension x="556" y="232"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="6" y="-4"/>
</Offset>
</Anchor>
</Anchors>
<FontString font="Fonts\FRIZQT__.TTF" justifyH="LEFT">
<FontHeight>
<AbsValue val="12"/>
</FontHeight>
<Color r="1" g="1" b="1"/>
</FontString>
</ScrollingMessageFrame>
</Frames>
</Frame>
<EditBox name="GlueConsoleInput" letters="2000" autoFocus="false">
<Size>
<AbsDimension x="568" y="20"/>
</Size>
<Anchors>
<Anchor point="BOTTOMLEFT">
<Offset>
<AbsDimension x="16" y="16"/>
</Offset>
</Anchor>
</Anchors>
<Backdrop bgFile="Interface\ChatFrame\ChatFrameBackground" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets>
<AbsInset left="4" right="4" top="4" bottom="4"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="16"/>
</TileSize>
<EdgeSize>
<AbsValue val="16"/>
</EdgeSize>
</Backdrop>
<FontString font="Fonts\FRIZQT__.TTF">
<FontHeight>
<AbsValue val="12"/>
</FontHeight>
<Color r="1" g="1" b="1"/>
</FontString>
<Scripts>
<OnEnterPressed>GlueConsole_Submit();</OnEnterPressed>
<OnEscapePressed>GlueConsole_Toggle();</OnEscapePressed>
</Scripts>
</EditBox>
</Frames>
<Scripts>
<OnLoad>GlueConsole_OnLoad();</OnLoad>
<OnMouseDown>this:StartMoving();</OnMouseDown>
<OnMouseUp>this:StopMovingOrSizing();</OnMouseUp>
</Scripts>
</Frame>
</Ui>