3 Commits

Author SHA1 Message Date
paste 8b7aa45813 Add MPQ release pipeline (packaged by mpq-packager on v* tag)
Release MPQ / package (push) Successful in 7s
2026-06-25 22:51:57 -05:00
paste e504f88167 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-06-22 23:40:37 -05:00
paste 13d77d77c9 Initial: Turtle WoW Glue XML baseline
Pristine baseline for the four Glue files we'll customize:
- AccountLogin.lua / .xml
- CharacterSelect.lua / .xml

Source: c:/WoW/Octo/BlizzardInterfaceCode/GlueXML (Turtle WoW).
Other files in BlizzardInterfaceCode/GlueXML are not tracked here since we don't override them on top of the MPQ-provided copies.
2026-06-22 23:31:46 -05:00
5 changed files with 1012 additions and 284 deletions
+107 -46
View File
@@ -249,7 +249,6 @@ function Autologin_PrevPage()
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;
@@ -258,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;
@@ -270,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);
@@ -286,6 +286,9 @@ function AccountLogin_OnShow()
AccountLoginRealmName:Hide()
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_PasswordFieldForced = false;
if ( table.getn(Autologin_Table) ~= 0 ) then
@@ -304,6 +307,9 @@ function AccountLogin_OnShow()
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()
@@ -327,7 +333,7 @@ function AccountLogin_OnKeyDown()
else
AccountLogin_Exit();
end
elseif ( arg1 == "ENTER" ) then
if ( not TOSAccepted() ) then
return;
@@ -357,34 +363,14 @@ function AccountLogin_Login()
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()
@@ -426,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)
@@ -439,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
@@ -450,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
+793 -105
View File
File diff suppressed because it is too large Load Diff
+62 -54
View File
@@ -8,6 +8,21 @@ 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()
this:SetSequence(0);
this:SetCamera(0);
@@ -44,7 +59,7 @@ function CharacterSelect_OnLoad()
local backdropColor = DEFAULT_TOOLTIP_COLOR;
CharacterSelectCharacterFrame:SetBackdropBorderColor(backdropColor[1], backdropColor[2], backdropColor[3]);
CharacterSelectCharacterFrame:SetBackdropColor(backdropColor[4], backdropColor[5], backdropColor[6], 0.85);
end
function CharacterSelect_OnShow()
@@ -57,7 +72,7 @@ function CharacterSelect_OnShow()
local serverType = "";
if ( serverName ) then
if( not connected ) then
serverName = serverName.."\n("..SERVER_DOWN..")";
serverName = serverName.."\n("..TEXT(SERVER_DOWN)..")";
end
if ( isPVP ) then
if ( isRP ) then
@@ -89,7 +104,7 @@ function CharacterSelect_OnShow()
else
local billingTimeLeft = GetBillingTimeRemaining();
-- Set default text for the payment plan
local billingText = _G["BILLING_TEXT"..paymentPlan];
local billingText = getglobal("BILLING_TEXT"..paymentPlan);
if ( paymentPlan == 1 ) then
-- Recurring account
billingTimeLeft = ceil(billingTimeLeft/(60 * 24));
@@ -100,7 +115,7 @@ function CharacterSelect_OnShow()
-- Free account
if ( billingTimeLeft < (24 * 60) ) then
billingText = format(BILLING_FREE_TIME_EXPIRE, billingTimeLeft.." "..GetText("MINUTES_ABBR", nil, billingTimeLeft));
end
end
elseif ( paymentPlan == 3 ) then
-- Fixed but not recurring
if ( isGameRoom == 1 ) then
@@ -115,7 +130,7 @@ function CharacterSelect_OnShow()
billingText = BILLING_FIXED_LASTDAY;
else
billingText = format(billingText, MinutesToTime(billingTimeLeft));
end
end
end
elseif ( paymentPlan == 4 ) then
-- Usage plan
@@ -188,23 +203,17 @@ function CharacterSelect_OnEvent()
elseif ( event == "CHARACTER_LIST_UPDATE" ) then
CharacterSelect_RebuildTranslationTable();
CharacterOrder_Apply();
CharacterOrder_Save();
if ( this.pendingSelectedCharID ) then
this.selectedIndex = GetIndexFromCharID(this.pendingSelectedCharID);
this.pendingSelectedCharID = nil;
end
UpdateCharacterList();
CharSelectCharacterName:SetText(GetCharacterInfo(GetCharIDFromIndex(this.selectedIndex)));
Autologin_OnCharactersLoad();
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
CharSelectCharacterName:SetText("");
else
CharSelectCharacterName:SetText(GetCharacterInfo(arg1));
if ( table.getn(this.translationTable) == 0 ) then
this.pendingSelectedCharID = arg1;
else
this.selectedIndex = GetIndexFromCharID(arg1);
end
this.selectedIndex = GetIndexFromCharID(arg1);
end
UpdateCharacterSelection();
elseif ( event == "SELECT_LAST_CHARACTER" ) then
@@ -219,9 +228,7 @@ function CharacterSelect_OnEvent()
GlueDialog_Show("SUGGEST_REALM");
elseif ( event == "FORCE_RENAME_CHARACTER" ) then
CharacterRenameDialog:Show();
CharacterRenameBackground:SetHeight(16 + CharacterRenameText1:GetHeight() + CharacterRenameText2:GetHeight() + 23 + CharacterRenameEditBox:GetHeight() + 8 + CharacterRenameButton1:GetHeight() + 16);
CharacterRenameText1:SetText(_G[arg1]);
CharacterRenameText1:SetText(getglobal(arg1));
end
end
@@ -231,50 +238,56 @@ function CharacterSelect_UpdateModel()
end
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
local btn = _G["CharSelectCharacterButton"..i];
local btn = getglobal("CharSelectCharacterButton"..i);
btn:UnlockHighlight();
if ( draggedIndex and i ~= draggedIndex ) then
btn:SetAlpha(0.4);
btn:SetAlpha(0.6);
else
btn:SetAlpha(1);
end
end
if ( (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
_G["CharSelectCharacterButton"..highlightIndex]:LockHighlight();
if ( highlightIndex and (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
getglobal("CharSelectCharacterButton"..highlightIndex):LockHighlight();
end
end
function UpdateCharacterList()
local numChars = GetNumCharacters();
local index = 1;
local coords;
for i=1, numChars, 1 do
local name, race, class, level, zone, fileString, gender, ghost = GetCharacterInfo(GetCharIDFromIndex(i));
local button = _G["CharSelectCharacterButton"..index];
if ( gender == 0 ) then
gender = "MALE";
else
gender = "FEMALE";
end
local button = getglobal("CharSelectCharacterButton"..index);
if ( not name ) then
button:SetText("ERROR - Tell Jeremy");
else
if ( not zone ) then
zone = "";
end
local classColor
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"
local classToken = TW_CLASS_TOKEN and TW_CLASS_TOKEN[class];
if ( classToken and CLASS_COLORS[classToken] ) then
class = CLASS_COLORS[classToken] .. class .. "|r";
end
_G["CharSelectCharacterButton"..index.."ButtonTextName"]:SetText(name);
if ( ghost ) then
_G["CharSelectCharacterButton"..index.."ButtonTextInfo"]:SetText(format(CHARACTER_SELECT_INFO_GHOST, level, class));
getglobal("CharSelectCharacterButton"..index.."ButtonTextName"):SetText(name);
if( ghost ) then
getglobal("CharSelectCharacterButton"..index.."ButtonTextInfo"):SetText(format(TEXT(CHARACTER_SELECT_INFO_GHOST), level, class));
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
_G["CharSelectCharacterButton"..index.."ButtonTextLocation"]:SetText(zone);
getglobal("CharSelectCharacterButton"..index.."ButtonTextLocation"):SetText(zone);
end
button:Show();
@@ -293,18 +306,18 @@ function UpdateCharacterList()
end
CharacterSelect.createIndex = 0;
CharSelectCreateCharacterButton:Hide();
CharSelectCreateCharacterButton:Hide();
local connected = IsConnectedToServer();
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
CharacterSelect.createIndex = index;
if ( connected ) then
--If can create characters position and show the create button
CharSelectCreateCharacterButton:SetID(index);
--CharSelectCreateCharacterButton:SetPoint("TOP", button, "TOP", 0, -5);
CharSelectCreateCharacterButton:Show();
CharSelectCreateCharacterButton:Show();
end
end
button:Hide();
@@ -332,6 +345,8 @@ function CharacterSelect_OnChar()
end
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;
@@ -355,10 +370,10 @@ function CharacterSelectButton_OnDoubleClick()
end
function CharacterSelect_TabResize()
local buttonMiddle = _G[this:GetName().."Middle"];
local buttonMiddleDisabled = _G[this:GetName().."MiddleDisabled"];
local buttonMiddle = getglobal(this:GetName().."Middle");
local buttonMiddleDisabled = getglobal(this:GetName().."MiddleDisabled");
local width = this:GetTextWidth() - 8;
local leftWidth = _G[this:GetName().."Left"]:GetWidth();
local leftWidth = getglobal(this:GetName().."Left"):GetWidth();
buttonMiddle:SetWidth(width);
buttonMiddleDisabled:SetWidth(width);
this:SetWidth(width + (2 * leftWidth));
@@ -386,14 +401,14 @@ end
function CharacterDeleteDialog_OnShow()
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);
CharacterDeleteButton1:Disable();
end
function CharacterSelect_EnterWorld()
PlaySound("gsCharacterSelectionEnterWorld");
EnterWorld();
Autologin_EnterWorld();
end
function CharacterSelect_Exit()
@@ -408,7 +423,7 @@ end
function CharacterSelect_TechSupport()
PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(TECH_SUPPORT_URL);
LaunchURL(TEXT(TECH_SUPPORT_URL));
end
function CharacterSelect_Delete()
@@ -469,10 +484,9 @@ end
function CharacterSelect_ManageAccount()
PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(AUTH_NO_TIME_URL);
LaunchURL(TEXT(AUTH_NO_TIME_URL));
end
-- ---------------------------------------------------------------------------
-- Character reordering
-- ---------------------------------------------------------------------------
@@ -685,12 +699,6 @@ function CharacterOrder_Save()
local realm = GetServerName() or "";
if ( realm == "" ) then return; end
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 = {};
for i = 1, numChars do
local name = GetCharacterInfo(GetCharIDFromIndex(i));
+50 -14
View File
@@ -125,16 +125,52 @@
CharacterSelect_EnterWorld();
</OnClick>
</Scripts>
<NormalText>
<Anchors>
<Anchor point="CENTER">
<Offset>
<AbsDimension x="-1" y="3"/>
</Offset>
</Anchor>
</Anchors>
</NormalText>
</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">
<Size>
<AbsDimension x="50" y="50"/>
@@ -607,7 +643,7 @@
<Anchors>
<Anchor point="LEFT">
<Offset>
<AbsDimension x="12" y="4"/>
<AbsDimension x="12" y="10"/>
</Offset>
</Anchor>
</Anchors>
@@ -615,7 +651,7 @@
</Layer>
</Layers>
<Frames>
<Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM">
<Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY">
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterDeleteBackground" relativePoint="BOTTOM">
<Offset>
@@ -695,7 +731,7 @@
else
CharacterDeleteButton1:Disable();
end
</OnTextChanged>
</OnTextChanged>
<OnEnterPressed>
if ( CharacterDeleteButton1:IsEnabled() == 1 ) then
CharacterSelect_DeleteCharacter();
@@ -775,7 +811,7 @@
<Anchors>
<Anchor point="LEFT">
<Offset>
<AbsDimension x="12" y="4"/>
<AbsDimension x="12" y="10"/>
</Offset>
</Anchor>
</Anchors>
@@ -783,7 +819,7 @@
</Layer>
</Layers>
<Frames>
<Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM">
<Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY">
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterRenameBackground" relativePoint="BOTTOM">
<Offset>
-65
View File
@@ -1,65 +0,0 @@
# 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.