8 Commits
Author SHA1 Message Date
paste 5ad529b465 Guard CharacterOrder_Save against empty char list
Release MPQ / package (push) Successful in 6s
CHARACTER_LIST_UPDATE can fire with numChars == 0 in a transient state
when returning to the glue screen after /logout, before the server's
SMSG_CHAR_ENUM re-populates the character list. Without this guard,
CharacterOrder_Save writes an empty string to the vault, wiping the
saved order. The real CHARACTER_LIST_UPDATE that follows then reads
that empty string in Apply, returns early, and leaves the table at
server-default identity order — sort order silently reset.

Fix: skip the write when numChars is 0. The only legitimate zero case
is a brand-new account with no characters, where there's nothing to
persist anyway.
2026-07-17 15:31:09 -05:00
paste 13ea626704 Trim customization to essentials
Release MPQ / package (push) Successful in 7s
Shrink the customization patch to only touch what's needed for the
autologin + character-reorder features, so upstream refreshes don't
conflict on areas we've been unnecessarily rewriting.

Reverted to upstream in AccountLogin.lua/.xml:
- Side-button stack (Cinematics + Turtle URL buttons) instead of our
  TOS/Credits/Community/ManageAccount replacements.
- Turtle URL helper functions (AccountLogin_Turtle_Website and siblings).
- turtlewow_expansion_logo texture (was swapped for Glues-WoW-Logo).
- AcceptTOS()/AcceptEULA() in OnShow instead of the full TOSFrame +
  four scroll-frames + EULA/Scanning/Contest dispatcher.
- VirtualKeypad layout, PIN display, random positioning, PIN
  remap-to-key-position logic.
- SurveyNotificationFrame at original location (TOSFrame block removed).

Reverted to upstream in CharacterSelect.lua/.xml:
- AutologinSaveCharacterButton checkbox (dead UI) and its two shim
  functions (Autologin_OnCharactersLoad, Autologin_EnterWorld).
- CharacterDelete/RenameButton1 text=CONFIRM (was OKAY).
- Dialog background text offsets.
- CharSelectEnterWorldButton NormalText block.
- Stylistic churn (_G[...] <-> getglobal, TEXT() wrapping,
  trailing whitespace, unused gender 0/1 -> MALE/FEMALE conversion).

Kept: AutologinAccountsFrame + AutologinAccountButtonTemplate + password
hide-on-known-account + Change password flow; drag-to-reorder +
translationTable + CharacterOrder persistence + pendingSelectedCharID
logout-slot fix + class-color hook (already in Turtle upstream, no
change) + drag mouse handlers on button template + delete/rename
wrapper functions.
2026-07-17 01:01:28 -05:00
paste fe78e6141b add readme 2026-07-17 00:13:13 -05:00
paste 09475b4698 Save CharacterOrder on CHARACTER_LIST_UPDATE 2026-07-17 00:13:13 -05:00
paste 51c987ccf2 fixed wrong character being selected upon logout 2026-07-17 00:13:13 -05:00
paste b460fb2be7 Add MPQ release pipeline (packaged by mpq-packager on v* tag) 2026-07-17 00:13:13 -05:00
paste 9f387a94f1 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).
2026-07-17 00:13:13 -05:00
paste 895c7b5a0a Initial: Turtle WoW Glue XML baseline
Pristine baseline for the four Glue files we customize:
- AccountLogin.lua / .xml
- CharacterSelect.lua / .xml

Source: c:/WoW/Octo/BlizzardInterfaceCode/GlueXML (Turtle WoW / Octo
upstream). Refreshed 2026-07-17. The prior baseline (13d77d7) pre-dates
upstream's simplification of the login side-button stack in
AccountLogin.xml (removed Credits, Reddit, Discord, Armory; inlined
LaunchURL calls to octowow.st URLs).

Other files in BlizzardInterfaceCode/GlueXML are not tracked here since
we don't override them on top of the MPQ-provided copies.
2026-07-17 00:10:38 -05:00
5 changed files with 271 additions and 1006 deletions
+46 -107
View File
@@ -249,6 +249,7 @@ function Autologin_PrevPage()
Autologin_UpdateUI(); Autologin_UpdateUI();
end 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;
@@ -257,13 +258,11 @@ 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(TEXT(VERSION_TEMPLATE), versionType, version, internalVersion, buildType, date)); AccountLoginVersion:SetText(format(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;
@@ -271,14 +270,15 @@ 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";
-- Try to show the EULA or the TOS AcceptTOS();
-- AccountLogin_ShowUserAgreements(); AcceptEULA();
local serverName = GetServerName(); local serverName = GetServerName();
if(serverName) then if(serverName) then
AccountLoginRealmName:SetText(serverName); AccountLoginRealmName:SetText(serverName);
@@ -286,9 +286,6 @@ function AccountLogin_OnShow()
AccountLoginRealmName:Hide() AccountLoginRealmName:Hide()
end end
-- 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_Load();
Autologin_PasswordFieldForced = false; Autologin_PasswordFieldForced = false;
if ( table.getn(Autologin_Table) ~= 0 ) then if ( table.getn(Autologin_Table) ~= 0 ) then
@@ -307,9 +304,6 @@ function AccountLogin_OnShow()
elseif ( AccountLoginPasswordEdit:IsVisible() ) then 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()
@@ -333,7 +327,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;
@@ -363,14 +357,34 @@ function AccountLogin_Login()
Autologin_OnLogin(); Autologin_OnLogin();
end end
function AccountLogin_ManageAccount() function AccountLogin_Turtle_Armory_Website()
PlaySound("gsLoginNewAccount"); PlaySound("gsLoginNewAccount");
LaunchURL(AUTH_NO_TIME_URL); LaunchURL(TURTLE_ARMORY_WEBSITE);
end end
function AccountLogin_LaunchCommunitySite() function AccountLogin_Turtle_Website()
PlaySound("gsLoginNewAccount"); PlaySound("gsLoginNewAccount");
LaunchURL(COMMUNITY_URL); 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);
end end
function AccountLogin_Credits() function AccountLogin_Credits()
@@ -412,73 +426,12 @@ 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
getglobal("VirtualKeypadButton"..i):SetText(getglobal("arg"..i)); buttonText[i] = _G["arg"..i]
end end
end end
-- Randomize location to prevent hacking (yeah right) -- Randomize location to prevent hacking (yeah right)
@@ -486,8 +439,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
@@ -497,45 +450,31 @@ 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 = VirtualKeypadFrame.PIN; local PIN = VirtualKeypadText:GetText();
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] = strsub(PIN,i,i); pinNumber[i] = nil;
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
+102 -790
View File
File diff suppressed because it is too large Load Diff
+44 -59
View File
@@ -8,21 +8,6 @@ MAX_CHARACTERS_PER_REALM = 10;
AUTO_DRAG_TIME = 0.5; 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);
@@ -59,7 +44,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()
@@ -72,7 +57,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("..TEXT(SERVER_DOWN)..")"; serverName = serverName.."\n("..SERVER_DOWN..")";
end end
if ( isPVP ) then if ( isPVP ) then
if ( isRP ) then if ( isRP ) then
@@ -104,7 +89,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 = getglobal("BILLING_TEXT"..paymentPlan); local billingText = _G["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));
@@ -115,7 +100,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
@@ -130,7 +115,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
@@ -210,7 +195,6 @@ function CharacterSelect_OnEvent()
end end
UpdateCharacterList(); UpdateCharacterList();
CharSelectCharacterName:SetText(GetCharacterInfo(GetCharIDFromIndex(this.selectedIndex))); CharSelectCharacterName:SetText(GetCharacterInfo(GetCharIDFromIndex(this.selectedIndex)));
Autologin_OnCharactersLoad();
elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then
if ( arg1 == 0 ) then if ( arg1 == 0 ) then
CharSelectCharacterName:SetText(""); CharSelectCharacterName:SetText("");
@@ -235,7 +219,9 @@ 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();
CharacterRenameText1:SetText(getglobal(arg1)); CharacterRenameBackground:SetHeight(16 + CharacterRenameText1:GetHeight() + CharacterRenameText2:GetHeight() + 23 + CharacterRenameEditBox:GetHeight() + 8 + CharacterRenameButton1:GetHeight() + 16);
CharacterRenameText1:SetText(_G[arg1]);
end end
end end
@@ -245,56 +231,50 @@ 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 draggedIndex = CharacterSelect.draggedIndex;
local highlightIndex = draggedIndex or CharacterSelect.selectedIndex; local highlightIndex = draggedIndex or CharacterSelect.selectedIndex;
for i=1, MAX_CHARACTERS_DISPLAYED, 1 do for i=1, MAX_CHARACTERS_DISPLAYED, 1 do
local btn = getglobal("CharSelectCharacterButton"..i); local btn = _G["CharSelectCharacterButton"..i];
btn:UnlockHighlight(); btn:UnlockHighlight();
if ( draggedIndex and i ~= draggedIndex ) then if ( draggedIndex and i ~= draggedIndex ) then
btn:SetAlpha(0.6); btn:SetAlpha(0.4);
else else
btn:SetAlpha(1); btn:SetAlpha(1);
end end
end end
if ( (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
if ( highlightIndex and (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then _G["CharSelectCharacterButton"..highlightIndex]:LockHighlight();
getglobal("CharSelectCharacterButton"..highlightIndex):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(GetCharIDFromIndex(i)); local name, race, class, level, zone, fileString, gender, ghost = GetCharacterInfo(GetCharIDFromIndex(i));
if ( gender == 0 ) then
gender = "MALE"; local button = _G["CharSelectCharacterButton"..index];
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];
if ( classToken and CLASS_COLORS[classToken] ) then local classColor
class = CLASS_COLORS[classToken] .. class .. "|r"; local classToken = TW_CLASS_TOKEN and TW_CLASS_TOKEN[class]
if classToken and CLASS_COLORS[classToken] then
classColor = CLASS_COLORS[classToken]
class = classColor .. class .. "|r"
end end
getglobal("CharSelectCharacterButton"..index.."ButtonTextName"):SetText(name); _G["CharSelectCharacterButton"..index.."ButtonTextName"]:SetText(name);
if( ghost ) then if ( ghost ) then
getglobal("CharSelectCharacterButton"..index.."ButtonTextInfo"):SetText(format(TEXT(CHARACTER_SELECT_INFO_GHOST), level, class)); _G["CharSelectCharacterButton"..index.."ButtonTextInfo"]:SetText(format(CHARACTER_SELECT_INFO_GHOST, level, class));
else else
getglobal("CharSelectCharacterButton"..index.."ButtonTextInfo"):SetText(format(TEXT(CHARACTER_SELECT_INFO), level, class)); _G["CharSelectCharacterButton"..index.."ButtonTextInfo"]:SetText(format(CHARACTER_SELECT_INFO, level, class));
end end
getglobal("CharSelectCharacterButton"..index.."ButtonTextLocation"):SetText(zone); _G["CharSelectCharacterButton"..index.."ButtonTextLocation"]:SetText(zone);
end end
button:Show(); button:Show();
@@ -313,18 +293,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 = getglobal("CharSelectCharacterButton"..index); local button = _G["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();
@@ -352,8 +332,6 @@ 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 if ( CharacterSelect.suppressNextClick ) then
CharacterSelect.suppressNextClick = nil; CharacterSelect.suppressNextClick = nil;
return; return;
@@ -377,10 +355,10 @@ function CharacterSelectButton_OnDoubleClick()
end end
function CharacterSelect_TabResize() function CharacterSelect_TabResize()
local buttonMiddle = getglobal(this:GetName().."Middle"); local buttonMiddle = _G[this:GetName().."Middle"];
local buttonMiddleDisabled = getglobal(this:GetName().."MiddleDisabled"); local buttonMiddleDisabled = _G[this:GetName().."MiddleDisabled"];
local width = this:GetTextWidth() - 8; local width = this:GetTextWidth() - 8;
local leftWidth = getglobal(this:GetName().."Left"):GetWidth(); local leftWidth = _G[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));
@@ -408,14 +386,14 @@ end
function CharacterDeleteDialog_OnShow() function CharacterDeleteDialog_OnShow()
local name, race, class, level = GetCharacterInfo(GetCharIDFromIndex(CharacterSelect.selectedIndex)); local name, race, class, level = GetCharacterInfo(GetCharIDFromIndex(CharacterSelect.selectedIndex));
CharacterDeleteText1:SetText(format(TEXT(CONFIRM_CHAR_DELETE), name, level, class)); CharacterDeleteText1:SetText(format(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");
Autologin_EnterWorld(); EnterWorld();
end end
function CharacterSelect_Exit() function CharacterSelect_Exit()
@@ -430,7 +408,7 @@ end
function CharacterSelect_TechSupport() function CharacterSelect_TechSupport()
PlaySound("gsCharacterSelectionAcctOptions"); PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(TEXT(TECH_SUPPORT_URL)); LaunchURL(TECH_SUPPORT_URL);
end end
function CharacterSelect_Delete() function CharacterSelect_Delete()
@@ -491,9 +469,10 @@ end
function CharacterSelect_ManageAccount() function CharacterSelect_ManageAccount()
PlaySound("gsCharacterSelectionAcctOptions"); PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(TEXT(AUTH_NO_TIME_URL)); LaunchURL(AUTH_NO_TIME_URL);
end end
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Character reordering -- Character reordering
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@@ -706,6 +685,12 @@ function CharacterOrder_Save()
local realm = GetServerName() or ""; local realm = GetServerName() or "";
if ( realm == "" ) then return; end if ( realm == "" ) then return; end
local numChars = GetNumCharacters(); local numChars = GetNumCharacters();
-- Skip when the list is empty. CHARACTER_LIST_UPDATE can fire in a
-- transient state (returning to glue after logout, before the server's
-- SMSG_CHAR_ENUM populates the list) where numChars is briefly 0.
-- Writing an empty string here would wipe the saved order before the
-- real update arrives to Apply it.
if ( numChars == 0 ) then return; end
local names = {}; local names = {};
for i = 1, numChars do for i = 1, numChars do
local name = GetCharacterInfo(GetCharIDFromIndex(i)); local name = GetCharacterInfo(GetCharIDFromIndex(i));
+14 -50
View File
@@ -125,52 +125,16 @@
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"/>
@@ -643,7 +607,7 @@
<Anchors> <Anchors>
<Anchor point="LEFT"> <Anchor point="LEFT">
<Offset> <Offset>
<AbsDimension x="12" y="10"/> <AbsDimension x="12" y="4"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>
@@ -651,7 +615,7 @@
</Layer> </Layer>
</Layers> </Layers>
<Frames> <Frames>
<Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY"> <Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM">
<Anchors> <Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterDeleteBackground" relativePoint="BOTTOM"> <Anchor point="BOTTOMRIGHT" relativeTo="CharacterDeleteBackground" relativePoint="BOTTOM">
<Offset> <Offset>
@@ -731,7 +695,7 @@
else else
CharacterDeleteButton1:Disable(); CharacterDeleteButton1:Disable();
end end
</OnTextChanged> </OnTextChanged>
<OnEnterPressed> <OnEnterPressed>
if ( CharacterDeleteButton1:IsEnabled() == 1 ) then if ( CharacterDeleteButton1:IsEnabled() == 1 ) then
CharacterSelect_DeleteCharacter(); CharacterSelect_DeleteCharacter();
@@ -811,7 +775,7 @@
<Anchors> <Anchors>
<Anchor point="LEFT"> <Anchor point="LEFT">
<Offset> <Offset>
<AbsDimension x="12" y="10"/> <AbsDimension x="12" y="4"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>
@@ -819,7 +783,7 @@
</Layer> </Layer>
</Layers> </Layers>
<Frames> <Frames>
<Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY"> <Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM">
<Anchors> <Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterRenameBackground" relativePoint="BOTTOM"> <Anchor point="BOTTOMRIGHT" relativeTo="CharacterRenameBackground" relativePoint="BOTTOM">
<Offset> <Offset>
+65
View File
@@ -0,0 +1,65 @@
# Octo GlueXML
Custom login and character-select UI for [Octo](https://octowow.st). Overlays
a small subset of the stock glue files with three features; everything else in
`Interface/GlueXML/` is loaded unchanged from the base MPQs.
## Features
- **Saved-account login.** The login screen shows a scrollable list of
previously-used accounts with their last-used timestamp. Clicking a row fills
the account name and hides the password field; a login without a typed
password logs in using the credential stored in the Windows Credential
Manager. A "Change password" button reveals the field on demand when you
need to update a saved credential.
- **Character reordering.** Hold on a character in the select screen for half
a second to enter drag mode, then move it up or down. Order is saved
per-realm and restored on the next login.
## ClassicAPI dependency
The saved-login and character-order features rely on Lua bindings exposed by
the ClassicAPI DLL, which is loaded alongside the client. Both features guard
their entry points with `type(fn) ~= "function"` checks so the UI still
functions when the DLL isn't present — you just lose persistence.
### Saved login — Windows Credential Manager
Passwords never live in Lua or in `WTF/`. They go straight from the password
field into the OS credential vault, and login submits are dispatched from C so
the plaintext never crosses back.
| Binding | Purpose |
|---|---|
| `SaveAccount(name, password)` | Writes a credential-vault entry. Called when the user types a password (either a first-time save or a "Change password" update). |
| `LoginWithSavedAccount(name)` | C-side dispatch of the login using the vault password for `name`. |
| `GetSavedAccounts()` | Returns `{ {name=…, lastUsed=epochSeconds}, … }` — `lastUsed` is the Windows `LastWritten` timestamp on the vault entry, auto-refreshed by `LoginWithSavedAccount`. |
| `DeleteAccount(name)` | Removes a vault entry (backs the "Remove account" button). |
| `GetSavedAccountName()` / `SetSavedAccountName(name)` | Persists just the *last-used* account name (via `SavedVariables`) so the same row is pre-selected on next launch. No password, no character map — those were dropped alongside the plaintext store. |
### Character sort order — per-realm WTF persistence
Persistence is keyed by character *name*, not by server-assigned charID. Slots
renumber every time a character is added or deleted, so name is the only
stable identifier.
| Binding | Purpose |
|---|---|
| `GetSavedCharacterOrder(realm)` | Reads a `\|`-separated name string for the current realm. |
| `SetSavedCharacterOrder(realm, names)` | Writes it back. |
On every `CHARACTER_LIST_UPDATE` the code loads the saved string, resolves
each name to the current charID, drops names the server no longer knows about
(deletions), appends any characters not in the saved list (creations), and
re-saves. That means the on-disk order self-cleans without a hook in the
delete path.
## Releases
Push a `v*` tag to trigger the
[mpq-packager](https://octowow.st/git/paste/mpq-packager) Gitea Action
(`.gitea/workflows/release.yml`), which packages this repo into a
`patch-Z.mpq` via StormLib according to `mpq.yaml`. `Z` sorts after Blizzard's
stock MPQs, so its files win on conflict. Only `.lua`, `.xml`, and `.toc`
files are placed in the MPQ — this README, workflow files, and build config
are excluded.