7 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
9 changed files with 1121 additions and 39 deletions
+63
View File
@@ -0,0 +1,63 @@
# Build patch-Z.mpq from this repo and attach it to a Gitea release on a v* tag.
# Packaged by paste/mpq-packager (pinned @v1).
#
# octowow.st is served under /git but Gitea generates URLs without it, which
# breaks actions/checkout and `uses:` resolution; and bind mounts don't work
# under act_runner (inner docker uses the host daemon). So we clone manually
# and run the packager via docker build + docker cp. Simplify once the server
# ROOT_URL is fixed.
name: Release MPQ
on:
push:
tags:
- "v*"
jobs:
package:
runs-on: ubuntu-latest
steps:
- name: Checkout (manual; instance under /git)
run: |
git init -q .
git remote add origin "https://gitea:${{ secrets.GITHUB_TOKEN }}@octowow.st/git/${{ github.repository }}.git"
git fetch -q --depth 1 origin "${{ github.ref }}"
git checkout -q FETCH_HEAD
- name: Build packager image
run: |
git clone -q --depth 1 --branch v1 \
"https://gitea:${{ secrets.GITHUB_TOKEN }}@octowow.st/git/paste/mpq-packager.git" /tmp/packager
docker build -q -t mpq-packager /tmp/packager
- name: Build patch MPQ
run: |
set -euo pipefail
cid=$(docker create \
-e INPUT_MANIFEST=mpq.yaml \
-e INPUT_VERSION=${{ github.ref_name }} \
-e INPUT_OUT_DIR=/work/dist \
-e GITHUB_WORKSPACE=/work \
mpq-packager)
docker cp . "$cid:/work"
docker start -a "$cid"
docker cp "$cid:/work/dist/patch-Z.mpq" ./patch-Z.mpq
docker rm "$cid" >/dev/null
ls -l patch-Z.mpq
- name: Create release and upload asset
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
API="https://octowow.st/git/api/v1/repos/${{ github.repository }}"
TAG="${{ github.ref_name }}"
RID=$(curl -fsSL -X POST "$API/releases" \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
curl -fsSL -X POST "$API/releases/$RID/assets?name=patch-Z.mpq" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@patch-Z.mpq"
echo "Released $TAG with patch-Z.mpq"
+267 -13
View File
@@ -1,3 +1,255 @@
-- 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;
DEFAULT_TOOLTIP_COLOR = {0.8, 0.8, 0.8, 0.09, 0.09, 0.09};
MAX_PIN_LENGTH = 10;
@@ -34,13 +286,22 @@ function AccountLogin_OnShow()
AccountLoginRealmName:Hide()
end
local accountName = GetSavedAccountName();
AccountLoginAccountEdit:SetText(accountName);
AccountLoginPasswordEdit:SetText("");
Autologin_Load();
Autologin_PasswordFieldForced = false;
if ( table.getn(Autologin_Table) ~= 0 ) then
local r = Autologin_Table[Autologin_SelectedIdx or 1];
if ( r ) then
AccountLoginAccountEdit:SetText(r.name);
AccountLoginPasswordEdit:SetText("");
Autologin_OnNameUpdate(r.name);
end
end
Autologin_UpdateUI();
Autologin_UpdatePasswordVisibility();
if ( accountName == "" ) then
if ( table.getn(Autologin_Table) == 0 ) then
AccountLogin_FocusAccountName();
else
elseif ( AccountLoginPasswordEdit:IsVisible() ) then
AccountLogin_FocusPassword();
end
end
@@ -93,14 +354,7 @@ end
function AccountLogin_Login()
PlaySound("gsLogin");
DefaultServerLogin(AccountLoginAccountEdit:GetText(), AccountLoginPasswordEdit:GetText());
AccountLoginPasswordEdit:SetText("");
if ( AccountLoginSaveAccountName:GetChecked() ) then
SetSavedAccountName(AccountLoginAccountEdit:GetText());
else
SetSavedAccountName("");
end
Autologin_OnLogin();
end
function AccountLogin_Turtle_Armory_Website()
+230 -5
View File
@@ -1,6 +1,7 @@
<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="AccountLogin.lua"/>
<Include file="GlueConsole.xml"/>
<Button name="VirtualKeyPadButtonTemplate" inherits="GlueMenuButtonTemplate" virtual="true">
<Size x="40" y="24"/>
<Scripts>
@@ -9,6 +10,77 @@
</OnClick>
</Scripts>
</Button>
<Button name="AutologinAccountButtonTemplate" virtual="true">
<Size>
<AbsDimension x="256" y="70"/>
</Size>
<HitRectInsets>
<AbsInset left="0" right="0" top="0" bottom="15"/>
</HitRectInsets>
<Frames>
<Frame name="$parentButtonText" setAllPoints="true">
<Layers>
<Layer level="BORDER">
<FontString name="$parentName" inherits="GlueFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="0" y="-5"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parentPassword" inherits="GlueFontHighlightSmall" justifyH="LEFT">
<Size>
<AbsDimension x="217" y="12"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentName" relativePoint="BOTTOMLEFT">
<Offset>
<AbsDimension x="0" y="-1"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parentCharacter" inherits="GlueFontDisableSmall" justifyH="LEFT">
<Size>
<AbsDimension x="217" y="16"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentPassword" relativePoint="BOTTOMLEFT">
<Offset>
<AbsDimension x="0" y="-1"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnClick>
AutologinAccountButton_OnClick();
</OnClick>
<OnDoubleClick>
AutologinAccountButton_OnDoubleClick();
</OnDoubleClick>
</Scripts>
<HighlightTexture file="Interface\Glues\CharacterSelect\Glue-CharacterSelect-Highlight" alphaMode="ADD">
<Size>
<AbsDimension x="256" y="74"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="-20" y="8"/>
</Offset>
</Anchor>
</Anchors>
</HighlightTexture>
<NormalFont inherits="GlueFontNormal"/>
<HighlightFont inherits="GlueFontHighlight"/>
</Button>
<ModelFFX name="AccountLogin" setAllPoints="true" file="Interface\Glues\Models\UI_MainMenu\UI_MainMenu.mdx" hidden="true" parent="GlueParent" enableKeyboard="true" fogNear="0" fogFar="1200">
<Frames>
<Frame name="AccountLoginUI" setAllPoints="true">
@@ -73,6 +145,163 @@
</Layer>
</Layers>
<Frames>
<Frame name="AutologinAccountsFrame">
<Size>
<AbsDimension x="260" y="330"/>
</Size>
<Anchors>
<Anchor point="TOPRIGHT">
<Offset>
<AbsDimension x="-5" y="-15"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString text="Select account" inherits="GlueFontDisableLarge">
<Size>
<AbsDimension x="256" y="13"/>
</Size>
<Anchors>
<Anchor point="TOP">
<Offset>
<AbsDimension x="0" y="-10"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<FontString name="AutologinSizeWarning" inherits="GlueFontRedSmall" text="WARNING: Too many accounts. Changes won't be saved correctly.">
<Size>
<AbsDimension x="220" y="28"/>
</Size>
<Anchors>
<Anchor point="TOP" relativeTo="AutologinAccountsFrame" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="4"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="AutologinAccountButton1" inherits="AutologinAccountButtonTemplate" id="1">
<Anchors>
<Anchor point="TOP" relativeTo="AutologinAccountsFrame" relativePoint="TOP">
<Offset>
<AbsDimension x="22" y="-30"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="AutologinAccountButton2" inherits="AutologinAccountButtonTemplate" id="2">
<Anchors>
<Anchor point="TOP" relativeTo="AutologinAccountButton1" relativePoint="BOTTOM">
<Offset>
<AbsDimension y="13"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="AutologinAccountButton3" inherits="AutologinAccountButtonTemplate" id="3">
<Anchors>
<Anchor point="TOP" relativeTo="AutologinAccountButton2" relativePoint="BOTTOM">
<Offset>
<AbsDimension y="13"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="AutologinAccountButton4" inherits="AutologinAccountButtonTemplate" id="4">
<Anchors>
<Anchor point="TOP" relativeTo="AutologinAccountButton3" relativePoint="BOTTOM">
<Offset>
<AbsDimension y="13"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Button name="AutologinRemoveAccountButton" inherits="GlueButtonSmallTemplate" text="Remove account">
<Anchors>
<Anchor point="BOTTOM" relativeTo="AutologinAccountsFrame" relativePoint="BOTTOM">
<Offset>
<AbsDimension x="0" y="5"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Autologin_RemoveAccount();
</OnClick>
</Scripts>
</Button>
<Button name="AutologinChangePasswordButton" inherits="GlueButtonSmallTemplate" text="Change password" hidden="true">
<Anchors>
<Anchor point="BOTTOM" relativeTo="AutologinRemoveAccountButton" relativePoint="TOP">
<Offset>
<AbsDimension x="0" y="-10"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Autologin_ChangePassword();
</OnClick>
</Scripts>
</Button>
<Button name="AutologinNextPage">
<Size>
<AbsDimension x="32" y="32"/>
</Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="AutologinAccountsFrame" relativePoint="BOTTOMRIGHT">
<Offset>
<AbsDimension x="-5" y="10"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Autologin_NextPage();
</OnClick>
</Scripts>
<NormalTexture file="Interface\Glues\Common\Glue-RightArrow-Button-Up"/>
<PushedTexture file="Interface\Glues\Common\Glue-RightArrow-Button-Down"/>
<HighlightTexture file="Interface\Glues\Common\Glue-RightArrow-Button-Highlight" alphaMode="ADD"/>
</Button>
<Button name="AutologinPrevPage">
<Size>
<AbsDimension x="32" y="32"/>
</Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="AutologinAccountsFrame" relativePoint="BOTTOMLEFT">
<Offset>
<AbsDimension x="10" y="10"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Autologin_PrevPage();
</OnClick>
</Scripts>
<NormalTexture file="Interface\Glues\Common\Glue-LeftArrow-Button-Up"/>
<PushedTexture file="Interface\Glues\Common\Glue-LeftArrow-Button-Down"/>
<HighlightTexture file="Interface\Glues\Common\Glue-LeftArrow-Button-Highlight" alphaMode="ADD"/>
</Button>
</Frames>
<Backdrop bgFile="Interface\Glues\Common\Glue-Tooltip-Background" edgeFile="Interface\Glues\Common\Glue-Tooltip-Border" tile="true">
<BackgroundInsets>
<AbsInset left="10" right="5" top="4" bottom="9"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="16"/>
</TileSize>
<EdgeSize>
<AbsValue val="16"/>
</EdgeSize>
</Backdrop>
</Frame>
<EditBox name="AccountLoginAccountEdit" letters="16">
<Size>
<AbsDimension x="160" y="37"/>
@@ -128,11 +357,7 @@
this:HighlightText();
</OnEditFocusGained>
<OnTextChanged>
local accountName = GetSavedAccountName();
if ( accountName ~= "" and accountName ~= this:GetText() ) then
SetSavedAccountName("");
AccountLoginSaveAccountName:SetChecked(0);
end
Autologin_OnNameUpdate(this:GetText());
</OnTextChanged>
</Scripts>
<FontString inherits="GlueEditBoxFont"/>
+282 -11
View File
@@ -6,6 +6,8 @@ CHARACTER_ROTATION_CONSTANT = 0.6;
MAX_CHARACTERS_DISPLAYED = 10;
MAX_CHARACTERS_PER_REALM = 10;
AUTO_DRAG_TIME = 0.5;
function CharacterSelect_OnLoad()
this:SetSequence(0);
this:SetCamera(0);
@@ -14,6 +16,12 @@ function CharacterSelect_OnLoad()
this.selectedIndex = 0;
this.selectLast = 0;
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("CHARACTER_LIST_UPDATE");
this:RegisterEvent("UPDATE_SELECTED_CHARACTER");
@@ -178,14 +186,25 @@ function CharacterSelect_OnEvent()
if ( event == "ADDON_LIST_UPDATE" ) then
UpdateAddonButton();
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(this.selectedIndex));
CharSelectCharacterName:SetText(GetCharacterInfo(GetCharIDFromIndex(this.selectedIndex)));
elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then
if ( arg1 == 0 ) then
CharSelectCharacterName:SetText("");
else
CharSelectCharacterName:SetText(GetCharacterInfo(arg1));
this.selectedIndex = arg1;
if ( table.getn(this.translationTable) == 0 ) then
this.pendingSelectedCharID = arg1;
else
this.selectedIndex = GetIndexFromCharID(arg1);
end
end
UpdateCharacterSelection();
elseif ( event == "SELECT_LAST_CHARACTER" ) then
@@ -212,13 +231,19 @@ function CharacterSelect_UpdateModel()
end
function UpdateCharacterSelection()
local draggedIndex = CharacterSelect.draggedIndex;
local highlightIndex = draggedIndex or CharacterSelect.selectedIndex;
for i=1, MAX_CHARACTERS_DISPLAYED, 1 do
_G["CharSelectCharacterButton"..i]:UnlockHighlight();
local btn = _G["CharSelectCharacterButton"..i];
btn:UnlockHighlight();
if ( draggedIndex and i ~= draggedIndex ) then
btn:SetAlpha(0.4);
else
btn:SetAlpha(1);
end
end
local index = this.selectedIndex;
if ( (index > 0) and (index <= MAX_CHARACTERS_DISPLAYED) )then
_G["CharSelectCharacterButton"..index]:LockHighlight();
if ( (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
_G["CharSelectCharacterButton"..highlightIndex]:LockHighlight();
end
end
@@ -227,7 +252,7 @@ function UpdateCharacterList()
local index = 1;
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));
local button = _G["CharSelectCharacterButton"..index];
if ( not name ) then
@@ -307,6 +332,10 @@ function CharacterSelect_OnChar()
end
function CharacterSelectButton_OnClick()
if ( CharacterSelect.suppressNextClick ) then
CharacterSelect.suppressNextClick = nil;
return;
end
local id = this:GetID();
if ( id ~= CharacterSelect.selectedIndex ) then
CharacterSelect_SelectCharacter(id);
@@ -314,6 +343,10 @@ function CharacterSelectButton_OnClick()
end
function CharacterSelectButton_OnDoubleClick()
if ( CharacterSelect.suppressNextClick ) then
CharacterSelect.suppressNextClick = nil;
return;
end
local id = this:GetID();
if ( id ~= CharacterSelect.selectedIndex ) then
CharacterSelect_SelectCharacter(id);
@@ -340,18 +373,19 @@ function CharacterSelect_SelectCharacter(id, noCreate)
SetGlueScreen("charcreate");
end
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
CharacterSelect.currentModel = fileString;
SetBackgroundModel(CharacterSelect, fileString);
end
SelectCharacter(id);
SelectCharacter(charID);
end
end
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));
CharacterDeleteBackground:SetHeight(16 + CharacterDeleteText1:GetHeight() + CharacterDeleteText2:GetHeight() + 23 + CharacterDeleteEditBox:GetHeight() + 8 + CharacterDeleteButton1:GetHeight() + 16);
CharacterDeleteButton1:Disable();
@@ -409,6 +443,16 @@ function CharacterSelectFrame_OnUpdate()
CHARACTER_SELECT_ROTATION_START_X = GetCursorPosition();
SetCharacterSelectFacing(GetCharacterSelectFacing() + diff);
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
function CharacterSelectRotateRight_OnUpdate()
@@ -427,3 +471,230 @@ function CharacterSelect_ManageAccount()
PlaySound("gsCharacterSelectionAcctOptions");
LaunchURL(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();
-- 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));
if ( name ) then table.insert(names, name); end
end
SetSavedCharacterOrder(realm, table.concat(names, "|"));
end
+10 -10
View File
@@ -56,6 +56,12 @@
<OnDoubleClick>
CharacterSelectButton_OnDoubleClick();
</OnDoubleClick>
<OnMouseDown>
CharacterSelectButton_OnMouseDown();
</OnMouseDown>
<OnMouseUp>
CharacterSelectButton_OnMouseUp();
</OnMouseUp>
</Scripts>
<HighlightTexture file="Interface\Glues\CharacterSelect\Glue-CharacterSelect-Highlight" alphaMode="ADD">
<Size>
@@ -619,8 +625,7 @@
</Anchors>
<Scripts>
<OnClick>
DeleteCharacter(CharacterSelect.selectedIndex);
CharacterDeleteDialog:Hide();
CharacterSelect_DeleteCharacter();
PlaySound("gsTitleOptionOK");
</OnClick>
</Scripts>
@@ -693,8 +698,7 @@
</OnTextChanged>
<OnEnterPressed>
if ( CharacterDeleteButton1:IsEnabled() == 1 ) then
DeleteCharacter(CharacterSelect.selectedIndex);
CharacterDeleteDialog:Hide();
CharacterSelect_DeleteCharacter();
end
</OnEnterPressed>
<OnEscapePressed>
@@ -789,9 +793,7 @@
</Anchors>
<Scripts>
<OnClick>
if ( RenameCharacter(CharacterSelect.selectedIndex, CharacterRenameEditBox:GetText()) ) then
CharacterRenameDialog:Hide();
end
CharacterSelect_RenameCharacter();
</OnClick>
</Scripts>
</Button>
@@ -854,9 +856,7 @@
</Layers>
<Scripts>
<OnEnterPressed>
if ( RenameCharacter(CharacterSelect.selectedIndex, CharacterRenameEditBox:GetText()) ) then
CharacterRenameDialog:Hide();
end
CharacterSelect_RenameCharacter();
</OnEnterPressed>
<OnEscapePressed>
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>
+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.
+20
View File
@@ -0,0 +1,20 @@
# Packs this repo into Interface\GlueXML\ inside a patch-Z.mpq.
# Z sorts after Blizzard's stock MPQs, so its files win on conflict.
# Built by paste/mpq-packager via .gitea/workflows/release.yml on a v* tag.
name: GlueXML
patch-letter: Z
substitute:
enabled: true
extensions: [".lua", ".xml", ".toc"]
contents:
- src: .
dest: Interface/GlueXML
ignore:
- ".git/**"
- ".gitea/**"
- "mpq.yaml"
- "*.md"
- "dist/**"