Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ad529b465 | |||
| 13ea626704 | |||
| fe78e6141b | |||
| 09475b4698 | |||
| 51c987ccf2 | |||
| b460fb2be7 | |||
| 9f387a94f1 |
@@ -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
@@ -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;
|
FADE_IN_TIME = 2;
|
||||||
DEFAULT_TOOLTIP_COLOR = {0.8, 0.8, 0.8, 0.09, 0.09, 0.09};
|
DEFAULT_TOOLTIP_COLOR = {0.8, 0.8, 0.8, 0.09, 0.09, 0.09};
|
||||||
MAX_PIN_LENGTH = 10;
|
MAX_PIN_LENGTH = 10;
|
||||||
@@ -34,13 +286,22 @@ function AccountLogin_OnShow()
|
|||||||
AccountLoginRealmName:Hide()
|
AccountLoginRealmName:Hide()
|
||||||
end
|
end
|
||||||
|
|
||||||
local accountName = GetSavedAccountName();
|
Autologin_Load();
|
||||||
AccountLoginAccountEdit:SetText(accountName);
|
Autologin_PasswordFieldForced = false;
|
||||||
AccountLoginPasswordEdit:SetText("");
|
if ( table.getn(Autologin_Table) ~= 0 ) then
|
||||||
|
local r = Autologin_Table[Autologin_SelectedIdx or 1];
|
||||||
|
if ( r ) then
|
||||||
|
AccountLoginAccountEdit:SetText(r.name);
|
||||||
|
AccountLoginPasswordEdit:SetText("");
|
||||||
|
Autologin_OnNameUpdate(r.name);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Autologin_UpdateUI();
|
||||||
|
Autologin_UpdatePasswordVisibility();
|
||||||
|
|
||||||
if ( accountName == "" ) then
|
if ( table.getn(Autologin_Table) == 0 ) then
|
||||||
AccountLogin_FocusAccountName();
|
AccountLogin_FocusAccountName();
|
||||||
else
|
elseif ( AccountLoginPasswordEdit:IsVisible() ) then
|
||||||
AccountLogin_FocusPassword();
|
AccountLogin_FocusPassword();
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -93,14 +354,7 @@ end
|
|||||||
|
|
||||||
function AccountLogin_Login()
|
function AccountLogin_Login()
|
||||||
PlaySound("gsLogin");
|
PlaySound("gsLogin");
|
||||||
DefaultServerLogin(AccountLoginAccountEdit:GetText(), AccountLoginPasswordEdit:GetText());
|
Autologin_OnLogin();
|
||||||
AccountLoginPasswordEdit:SetText("");
|
|
||||||
|
|
||||||
if ( AccountLoginSaveAccountName:GetChecked() ) then
|
|
||||||
SetSavedAccountName(AccountLoginAccountEdit:GetText());
|
|
||||||
else
|
|
||||||
SetSavedAccountName("");
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function AccountLogin_Turtle_Armory_Website()
|
function AccountLogin_Turtle_Armory_Website()
|
||||||
|
|||||||
+230
-5
@@ -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/
|
<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">
|
..\FrameXML\UI.xsd">
|
||||||
<Script file="AccountLogin.lua"/>
|
<Script file="AccountLogin.lua"/>
|
||||||
|
<Include file="GlueConsole.xml"/>
|
||||||
<Button name="VirtualKeyPadButtonTemplate" inherits="GlueMenuButtonTemplate" virtual="true">
|
<Button name="VirtualKeyPadButtonTemplate" inherits="GlueMenuButtonTemplate" virtual="true">
|
||||||
<Size x="40" y="24"/>
|
<Size x="40" y="24"/>
|
||||||
<Scripts>
|
<Scripts>
|
||||||
@@ -9,6 +10,77 @@
|
|||||||
</OnClick>
|
</OnClick>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Button>
|
</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">
|
<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>
|
<Frames>
|
||||||
<Frame name="AccountLoginUI" setAllPoints="true">
|
<Frame name="AccountLoginUI" setAllPoints="true">
|
||||||
@@ -73,6 +145,163 @@
|
|||||||
</Layer>
|
</Layer>
|
||||||
</Layers>
|
</Layers>
|
||||||
<Frames>
|
<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">
|
<EditBox name="AccountLoginAccountEdit" letters="16">
|
||||||
<Size>
|
<Size>
|
||||||
<AbsDimension x="160" y="37"/>
|
<AbsDimension x="160" y="37"/>
|
||||||
@@ -128,11 +357,7 @@
|
|||||||
this:HighlightText();
|
this:HighlightText();
|
||||||
</OnEditFocusGained>
|
</OnEditFocusGained>
|
||||||
<OnTextChanged>
|
<OnTextChanged>
|
||||||
local accountName = GetSavedAccountName();
|
Autologin_OnNameUpdate(this:GetText());
|
||||||
if ( accountName ~= "" and accountName ~= this:GetText() ) then
|
|
||||||
SetSavedAccountName("");
|
|
||||||
AccountLoginSaveAccountName:SetChecked(0);
|
|
||||||
end
|
|
||||||
</OnTextChanged>
|
</OnTextChanged>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
<FontString inherits="GlueEditBoxFont"/>
|
<FontString inherits="GlueEditBoxFont"/>
|
||||||
|
|||||||
+282
-11
@@ -6,6 +6,8 @@ CHARACTER_ROTATION_CONSTANT = 0.6;
|
|||||||
MAX_CHARACTERS_DISPLAYED = 10;
|
MAX_CHARACTERS_DISPLAYED = 10;
|
||||||
MAX_CHARACTERS_PER_REALM = 10;
|
MAX_CHARACTERS_PER_REALM = 10;
|
||||||
|
|
||||||
|
AUTO_DRAG_TIME = 0.5;
|
||||||
|
|
||||||
function CharacterSelect_OnLoad()
|
function CharacterSelect_OnLoad()
|
||||||
this:SetSequence(0);
|
this:SetSequence(0);
|
||||||
this:SetCamera(0);
|
this:SetCamera(0);
|
||||||
@@ -14,6 +16,12 @@ function CharacterSelect_OnLoad()
|
|||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
this.selectLast = 0;
|
this.selectLast = 0;
|
||||||
this.currentModel = "";
|
this.currentModel = "";
|
||||||
|
this.translationTable = {};
|
||||||
|
this.orderChanged = nil;
|
||||||
|
this.pressDownButton = nil;
|
||||||
|
this.pressDownTime = 0;
|
||||||
|
this.draggedIndex = nil;
|
||||||
|
this.suppressNextClick = nil;
|
||||||
this:RegisterEvent("ADDON_LIST_UPDATE");
|
this:RegisterEvent("ADDON_LIST_UPDATE");
|
||||||
this:RegisterEvent("CHARACTER_LIST_UPDATE");
|
this:RegisterEvent("CHARACTER_LIST_UPDATE");
|
||||||
this:RegisterEvent("UPDATE_SELECTED_CHARACTER");
|
this:RegisterEvent("UPDATE_SELECTED_CHARACTER");
|
||||||
@@ -178,14 +186,25 @@ function CharacterSelect_OnEvent()
|
|||||||
if ( event == "ADDON_LIST_UPDATE" ) then
|
if ( event == "ADDON_LIST_UPDATE" ) then
|
||||||
UpdateAddonButton();
|
UpdateAddonButton();
|
||||||
elseif ( event == "CHARACTER_LIST_UPDATE" ) then
|
elseif ( event == "CHARACTER_LIST_UPDATE" ) then
|
||||||
|
CharacterSelect_RebuildTranslationTable();
|
||||||
|
CharacterOrder_Apply();
|
||||||
|
CharacterOrder_Save();
|
||||||
|
if ( this.pendingSelectedCharID ) then
|
||||||
|
this.selectedIndex = GetIndexFromCharID(this.pendingSelectedCharID);
|
||||||
|
this.pendingSelectedCharID = nil;
|
||||||
|
end
|
||||||
UpdateCharacterList();
|
UpdateCharacterList();
|
||||||
CharSelectCharacterName:SetText(GetCharacterInfo(this.selectedIndex));
|
CharSelectCharacterName:SetText(GetCharacterInfo(GetCharIDFromIndex(this.selectedIndex)));
|
||||||
elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then
|
elseif ( event == "UPDATE_SELECTED_CHARACTER" ) then
|
||||||
if ( arg1 == 0 ) then
|
if ( arg1 == 0 ) then
|
||||||
CharSelectCharacterName:SetText("");
|
CharSelectCharacterName:SetText("");
|
||||||
else
|
else
|
||||||
CharSelectCharacterName:SetText(GetCharacterInfo(arg1));
|
CharSelectCharacterName:SetText(GetCharacterInfo(arg1));
|
||||||
this.selectedIndex = arg1;
|
if ( table.getn(this.translationTable) == 0 ) then
|
||||||
|
this.pendingSelectedCharID = arg1;
|
||||||
|
else
|
||||||
|
this.selectedIndex = GetIndexFromCharID(arg1);
|
||||||
|
end
|
||||||
end
|
end
|
||||||
UpdateCharacterSelection();
|
UpdateCharacterSelection();
|
||||||
elseif ( event == "SELECT_LAST_CHARACTER" ) then
|
elseif ( event == "SELECT_LAST_CHARACTER" ) then
|
||||||
@@ -212,13 +231,19 @@ function CharacterSelect_UpdateModel()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function UpdateCharacterSelection()
|
function UpdateCharacterSelection()
|
||||||
|
local draggedIndex = CharacterSelect.draggedIndex;
|
||||||
|
local highlightIndex = draggedIndex or CharacterSelect.selectedIndex;
|
||||||
for i=1, MAX_CHARACTERS_DISPLAYED, 1 do
|
for i=1, MAX_CHARACTERS_DISPLAYED, 1 do
|
||||||
_G["CharSelectCharacterButton"..i]:UnlockHighlight();
|
local btn = _G["CharSelectCharacterButton"..i];
|
||||||
|
btn:UnlockHighlight();
|
||||||
|
if ( draggedIndex and i ~= draggedIndex ) then
|
||||||
|
btn:SetAlpha(0.4);
|
||||||
|
else
|
||||||
|
btn:SetAlpha(1);
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
if ( (highlightIndex > 0) and (highlightIndex <= MAX_CHARACTERS_DISPLAYED) ) then
|
||||||
local index = this.selectedIndex;
|
_G["CharSelectCharacterButton"..highlightIndex]:LockHighlight();
|
||||||
if ( (index > 0) and (index <= MAX_CHARACTERS_DISPLAYED) )then
|
|
||||||
_G["CharSelectCharacterButton"..index]:LockHighlight();
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -227,7 +252,7 @@ function UpdateCharacterList()
|
|||||||
local index = 1;
|
local index = 1;
|
||||||
|
|
||||||
for i=1, numChars, 1 do
|
for i=1, numChars, 1 do
|
||||||
local name, race, class, level, zone, fileString, gender, ghost = GetCharacterInfo(i);
|
local name, race, class, level, zone, fileString, gender, ghost = GetCharacterInfo(GetCharIDFromIndex(i));
|
||||||
|
|
||||||
local button = _G["CharSelectCharacterButton"..index];
|
local button = _G["CharSelectCharacterButton"..index];
|
||||||
if ( not name ) then
|
if ( not name ) then
|
||||||
@@ -307,6 +332,10 @@ function CharacterSelect_OnChar()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function CharacterSelectButton_OnClick()
|
function CharacterSelectButton_OnClick()
|
||||||
|
if ( CharacterSelect.suppressNextClick ) then
|
||||||
|
CharacterSelect.suppressNextClick = nil;
|
||||||
|
return;
|
||||||
|
end
|
||||||
local id = this:GetID();
|
local id = this:GetID();
|
||||||
if ( id ~= CharacterSelect.selectedIndex ) then
|
if ( id ~= CharacterSelect.selectedIndex ) then
|
||||||
CharacterSelect_SelectCharacter(id);
|
CharacterSelect_SelectCharacter(id);
|
||||||
@@ -314,6 +343,10 @@ function CharacterSelectButton_OnClick()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function CharacterSelectButton_OnDoubleClick()
|
function CharacterSelectButton_OnDoubleClick()
|
||||||
|
if ( CharacterSelect.suppressNextClick ) then
|
||||||
|
CharacterSelect.suppressNextClick = nil;
|
||||||
|
return;
|
||||||
|
end
|
||||||
local id = this:GetID();
|
local id = this:GetID();
|
||||||
if ( id ~= CharacterSelect.selectedIndex ) then
|
if ( id ~= CharacterSelect.selectedIndex ) then
|
||||||
CharacterSelect_SelectCharacter(id);
|
CharacterSelect_SelectCharacter(id);
|
||||||
@@ -340,18 +373,19 @@ function CharacterSelect_SelectCharacter(id, noCreate)
|
|||||||
SetGlueScreen("charcreate");
|
SetGlueScreen("charcreate");
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
local name, race, class, level, zone, fileString = GetCharacterInfo(id);
|
local charID = GetCharIDFromIndex(id);
|
||||||
|
local name, race, class, level, zone, fileString = GetCharacterInfo(charID);
|
||||||
|
|
||||||
if ( fileString ~= CharacterSelect.currentModel ) then
|
if ( fileString ~= CharacterSelect.currentModel ) then
|
||||||
CharacterSelect.currentModel = fileString;
|
CharacterSelect.currentModel = fileString;
|
||||||
SetBackgroundModel(CharacterSelect, fileString);
|
SetBackgroundModel(CharacterSelect, fileString);
|
||||||
end
|
end
|
||||||
SelectCharacter(id);
|
SelectCharacter(charID);
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function CharacterDeleteDialog_OnShow()
|
function CharacterDeleteDialog_OnShow()
|
||||||
local name, race, class, level = GetCharacterInfo(CharacterSelect.selectedIndex);
|
local name, race, class, level = GetCharacterInfo(GetCharIDFromIndex(CharacterSelect.selectedIndex));
|
||||||
CharacterDeleteText1:SetText(format(CONFIRM_CHAR_DELETE, name, level, class));
|
CharacterDeleteText1:SetText(format(CONFIRM_CHAR_DELETE, name, level, class));
|
||||||
CharacterDeleteBackground:SetHeight(16 + CharacterDeleteText1:GetHeight() + CharacterDeleteText2:GetHeight() + 23 + CharacterDeleteEditBox:GetHeight() + 8 + CharacterDeleteButton1:GetHeight() + 16);
|
CharacterDeleteBackground:SetHeight(16 + CharacterDeleteText1:GetHeight() + CharacterDeleteText2:GetHeight() + 23 + CharacterDeleteEditBox:GetHeight() + 8 + CharacterDeleteButton1:GetHeight() + 16);
|
||||||
CharacterDeleteButton1:Disable();
|
CharacterDeleteButton1:Disable();
|
||||||
@@ -409,6 +443,16 @@ function CharacterSelectFrame_OnUpdate()
|
|||||||
CHARACTER_SELECT_ROTATION_START_X = GetCursorPosition();
|
CHARACTER_SELECT_ROTATION_START_X = GetCursorPosition();
|
||||||
SetCharacterSelectFacing(GetCharacterSelectFacing() + diff);
|
SetCharacterSelectFacing(GetCharacterSelectFacing() + diff);
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Press-and-hold detector: a character button held past AUTO_DRAG_TIME
|
||||||
|
-- promotes into drag mode. Released sooner, OnMouseUp clears the
|
||||||
|
-- button and the click flows normally.
|
||||||
|
if ( CharacterSelect.pressDownButton ) then
|
||||||
|
CharacterSelect.pressDownTime = CharacterSelect.pressDownTime + arg1;
|
||||||
|
if ( CharacterSelect.pressDownTime >= AUTO_DRAG_TIME ) then
|
||||||
|
CharacterSelectButton_OnDragStart(CharacterSelect.pressDownButton);
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function CharacterSelectRotateRight_OnUpdate()
|
function CharacterSelectRotateRight_OnUpdate()
|
||||||
@@ -427,3 +471,230 @@ function CharacterSelect_ManageAccount()
|
|||||||
PlaySound("gsCharacterSelectionAcctOptions");
|
PlaySound("gsCharacterSelectionAcctOptions");
|
||||||
LaunchURL(AUTH_NO_TIME_URL);
|
LaunchURL(AUTH_NO_TIME_URL);
|
||||||
end
|
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
@@ -56,6 +56,12 @@
|
|||||||
<OnDoubleClick>
|
<OnDoubleClick>
|
||||||
CharacterSelectButton_OnDoubleClick();
|
CharacterSelectButton_OnDoubleClick();
|
||||||
</OnDoubleClick>
|
</OnDoubleClick>
|
||||||
|
<OnMouseDown>
|
||||||
|
CharacterSelectButton_OnMouseDown();
|
||||||
|
</OnMouseDown>
|
||||||
|
<OnMouseUp>
|
||||||
|
CharacterSelectButton_OnMouseUp();
|
||||||
|
</OnMouseUp>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
<HighlightTexture file="Interface\Glues\CharacterSelect\Glue-CharacterSelect-Highlight" alphaMode="ADD">
|
<HighlightTexture file="Interface\Glues\CharacterSelect\Glue-CharacterSelect-Highlight" alphaMode="ADD">
|
||||||
<Size>
|
<Size>
|
||||||
@@ -619,8 +625,7 @@
|
|||||||
</Anchors>
|
</Anchors>
|
||||||
<Scripts>
|
<Scripts>
|
||||||
<OnClick>
|
<OnClick>
|
||||||
DeleteCharacter(CharacterSelect.selectedIndex);
|
CharacterSelect_DeleteCharacter();
|
||||||
CharacterDeleteDialog:Hide();
|
|
||||||
PlaySound("gsTitleOptionOK");
|
PlaySound("gsTitleOptionOK");
|
||||||
</OnClick>
|
</OnClick>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
@@ -693,8 +698,7 @@
|
|||||||
</OnTextChanged>
|
</OnTextChanged>
|
||||||
<OnEnterPressed>
|
<OnEnterPressed>
|
||||||
if ( CharacterDeleteButton1:IsEnabled() == 1 ) then
|
if ( CharacterDeleteButton1:IsEnabled() == 1 ) then
|
||||||
DeleteCharacter(CharacterSelect.selectedIndex);
|
CharacterSelect_DeleteCharacter();
|
||||||
CharacterDeleteDialog:Hide();
|
|
||||||
end
|
end
|
||||||
</OnEnterPressed>
|
</OnEnterPressed>
|
||||||
<OnEscapePressed>
|
<OnEscapePressed>
|
||||||
@@ -789,9 +793,7 @@
|
|||||||
</Anchors>
|
</Anchors>
|
||||||
<Scripts>
|
<Scripts>
|
||||||
<OnClick>
|
<OnClick>
|
||||||
if ( RenameCharacter(CharacterSelect.selectedIndex, CharacterRenameEditBox:GetText()) ) then
|
CharacterSelect_RenameCharacter();
|
||||||
CharacterRenameDialog:Hide();
|
|
||||||
end
|
|
||||||
</OnClick>
|
</OnClick>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -854,9 +856,7 @@
|
|||||||
</Layers>
|
</Layers>
|
||||||
<Scripts>
|
<Scripts>
|
||||||
<OnEnterPressed>
|
<OnEnterPressed>
|
||||||
if ( RenameCharacter(CharacterSelect.selectedIndex, CharacterRenameEditBox:GetText()) ) then
|
CharacterSelect_RenameCharacter();
|
||||||
CharacterRenameDialog:Hide();
|
|
||||||
end
|
|
||||||
</OnEnterPressed>
|
</OnEnterPressed>
|
||||||
<OnEscapePressed>
|
<OnEscapePressed>
|
||||||
CharacterRenameDialog:Hide();
|
CharacterRenameDialog:Hide();
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
@@ -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.
|
||||||
@@ -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/**"
|
||||||
Reference in New Issue
Block a user