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();
end
FADE_IN_TIME = 2;
DEFAULT_TOOLTIP_COLOR = {0.8, 0.8, 0.8, 0.09, 0.09, 0.09};
MAX_PIN_LENGTH = 10;
@@ -257,13 +258,11 @@ 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(TEXT(VERSION_TEMPLATE), versionType, version, internalVersion, buildType, date));
AccountLoginVersion:SetText(format(VERSION_TEMPLATE, versionType, version, internalVersion, buildType, date));
-- Color edit box backdrops
local backdropColor = DEFAULT_TOOLTIP_COLOR;
@@ -271,14 +270,15 @@ 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";
-- Try to show the EULA or the TOS
-- AccountLogin_ShowUserAgreements();
AcceptTOS();
AcceptEULA();
local serverName = GetServerName();
if(serverName) then
AccountLoginRealmName:SetText(serverName);
@@ -286,9 +286,6 @@ 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
@@ -307,9 +304,6 @@ 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()
@@ -333,7 +327,7 @@ function AccountLogin_OnKeyDown()
else
AccountLogin_Exit();
end
elseif ( arg1 == "ENTER" ) then
if ( not TOSAccepted() ) then
return;
@@ -363,14 +357,34 @@ function AccountLogin_Login()
Autologin_OnLogin();
end
function AccountLogin_ManageAccount()
function AccountLogin_Turtle_Armory_Website()
PlaySound("gsLoginNewAccount");
LaunchURL(AUTH_NO_TIME_URL);
LaunchURL(TURTLE_ARMORY_WEBSITE);
end
function AccountLogin_LaunchCommunitySite()
function AccountLogin_Turtle_Website()
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
function AccountLogin_Credits()
@@ -412,73 +426,12 @@ 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
getglobal("VirtualKeypadButton"..i):SetText(getglobal("arg"..i));
buttonText[i] = _G["arg"..i]
end
end
-- Randomize location to prevent hacking (yeah right)
@@ -486,8 +439,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
@@ -497,45 +450,31 @@ 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 = VirtualKeypadFrame.PIN;
local PIN = VirtualKeypadText:GetText();
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] = 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
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
+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;
-- 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);
@@ -59,7 +44,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()
@@ -72,7 +57,7 @@ function CharacterSelect_OnShow()
local serverType = "";
if ( serverName ) then
if( not connected ) then
serverName = serverName.."\n("..TEXT(SERVER_DOWN)..")";
serverName = serverName.."\n("..SERVER_DOWN..")";
end
if ( isPVP ) then
if ( isRP ) then
@@ -104,7 +89,7 @@ function CharacterSelect_OnShow()
else
local billingTimeLeft = GetBillingTimeRemaining();
-- Set default text for the payment plan
local billingText = getglobal("BILLING_TEXT"..paymentPlan);
local billingText = _G["BILLING_TEXT"..paymentPlan];
if ( paymentPlan == 1 ) then
-- Recurring account
billingTimeLeft = ceil(billingTimeLeft/(60 * 24));
@@ -115,7 +100,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
@@ -130,7 +115,7 @@ function CharacterSelect_OnShow()
billingText = BILLING_FIXED_LASTDAY;
else
billingText = format(billingText, MinutesToTime(billingTimeLeft));
end
end
end
elseif ( paymentPlan == 4 ) then
-- Usage plan
@@ -210,7 +195,6 @@ function CharacterSelect_OnEvent()
end
UpdateCharacterList();
CharSelectCharacterName:SetText(GetCharacterInfo(GetCharIDFromIndex(this.selectedIndex)));
Autologin_OnCharactersLoad();
elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then
if ( arg1 == 0 ) then
CharSelectCharacterName:SetText("");
@@ -235,7 +219,9 @@ function CharacterSelect_OnEvent()
GlueDialog_Show("SUGGEST_REALM");
elseif ( event == "FORCE_RENAME_CHARACTER" ) then
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
@@ -245,56 +231,50 @@ 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 = getglobal("CharSelectCharacterButton"..i);
local btn = _G["CharSelectCharacterButton"..i];
btn:UnlockHighlight();
if ( draggedIndex and i ~= draggedIndex ) then
btn:SetAlpha(0.6);
btn:SetAlpha(0.4);
else
btn:SetAlpha(1);
end
end
if ( highlightIndex and (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
getglobal("CharSelectCharacterButton"..highlightIndex):LockHighlight();
if ( (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
_G["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));
if ( gender == 0 ) then
gender = "MALE";
else
gender = "FEMALE";
end
local button = getglobal("CharSelectCharacterButton"..index);
local button = _G["CharSelectCharacterButton"..index];
if ( not name ) then
button:SetText("ERROR - Tell Jeremy");
else
if ( not zone ) then
zone = "";
end
local classToken = TW_CLASS_TOKEN and TW_CLASS_TOKEN[class];
if ( classToken and CLASS_COLORS[classToken] ) then
class = CLASS_COLORS[classToken] .. class .. "|r";
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"
end
getglobal("CharSelectCharacterButton"..index.."ButtonTextName"):SetText(name);
if( ghost ) then
getglobal("CharSelectCharacterButton"..index.."ButtonTextInfo"):SetText(format(TEXT(CHARACTER_SELECT_INFO_GHOST), level, class));
_G["CharSelectCharacterButton"..index.."ButtonTextName"]:SetText(name);
if ( ghost ) then
_G["CharSelectCharacterButton"..index.."ButtonTextInfo"]:SetText(format(CHARACTER_SELECT_INFO_GHOST, level, class));
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
getglobal("CharSelectCharacterButton"..index.."ButtonTextLocation"):SetText(zone);
_G["CharSelectCharacterButton"..index.."ButtonTextLocation"]:SetText(zone);
end
button:Show();
@@ -313,18 +293,18 @@ function UpdateCharacterList()
end
CharacterSelect.createIndex = 0;
CharSelectCreateCharacterButton:Hide();
CharSelectCreateCharacterButton:Hide();
local connected = IsConnectedToServer();
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
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();
@@ -352,8 +332,6 @@ 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;
@@ -377,10 +355,10 @@ function CharacterSelectButton_OnDoubleClick()
end
function CharacterSelect_TabResize()
local buttonMiddle = getglobal(this:GetName().."Middle");
local buttonMiddleDisabled = getglobal(this:GetName().."MiddleDisabled");
local buttonMiddle = _G[this:GetName().."Middle"];
local buttonMiddleDisabled = _G[this:GetName().."MiddleDisabled"];
local width = this:GetTextWidth() - 8;
local leftWidth = getglobal(this:GetName().."Left"):GetWidth();
local leftWidth = _G[this:GetName().."Left"]:GetWidth();
buttonMiddle:SetWidth(width);
buttonMiddleDisabled:SetWidth(width);
this:SetWidth(width + (2 * leftWidth));
@@ -408,14 +386,14 @@ end
function CharacterDeleteDialog_OnShow()
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);
CharacterDeleteButton1:Disable();
end
function CharacterSelect_EnterWorld()
PlaySound("gsCharacterSelectionEnterWorld");
Autologin_EnterWorld();
EnterWorld();
end
function CharacterSelect_Exit()
@@ -430,7 +408,7 @@ end
function CharacterSelect_TechSupport()
PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(TEXT(TECH_SUPPORT_URL));
LaunchURL(TECH_SUPPORT_URL);
end
function CharacterSelect_Delete()
@@ -491,9 +469,10 @@ end
function CharacterSelect_ManageAccount()
PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(TEXT(AUTH_NO_TIME_URL));
LaunchURL(AUTH_NO_TIME_URL);
end
-- ---------------------------------------------------------------------------
-- Character reordering
-- ---------------------------------------------------------------------------
@@ -706,6 +685,12 @@ 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));
+14 -50
View File
@@ -125,52 +125,16 @@
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"/>
@@ -643,7 +607,7 @@
<Anchors>
<Anchor point="LEFT">
<Offset>
<AbsDimension x="12" y="10"/>
<AbsDimension x="12" y="4"/>
</Offset>
</Anchor>
</Anchors>
@@ -651,7 +615,7 @@
</Layer>
</Layers>
<Frames>
<Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY">
<Button name="CharacterDeleteButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM">
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterDeleteBackground" relativePoint="BOTTOM">
<Offset>
@@ -731,7 +695,7 @@
else
CharacterDeleteButton1:Disable();
end
</OnTextChanged>
</OnTextChanged>
<OnEnterPressed>
if ( CharacterDeleteButton1:IsEnabled() == 1 ) then
CharacterSelect_DeleteCharacter();
@@ -811,7 +775,7 @@
<Anchors>
<Anchor point="LEFT">
<Offset>
<AbsDimension x="12" y="10"/>
<AbsDimension x="12" y="4"/>
</Offset>
</Anchor>
</Anchors>
@@ -819,7 +783,7 @@
</Layer>
</Layers>
<Frames>
<Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="OKAY">
<Button name="CharacterRenameButton1" inherits="GlueDialogButtonTemplate" id="1" text="CONFIRM">
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="CharacterRenameBackground" relativePoint="BOTTOM">
<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.