diff --git a/AUDIT.md b/AUDIT.md index 3b5f2c4..e430be8 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -17,6 +17,12 @@ Target: Turtle WoW 1.18.x / Octo client. - **Calendar tooltip:** disabled/empty calendar days no longer concatenate a nil/stale `mails` value. - **OnUpdate rebinding:** `TurtleMail:init()` binds the original `on_update` function before the compatibility file loads; the patch explicitly rebinds the frame script so the safer handler is actually used. + +## Fixed in 1.4.8 + +- **`MailMailButton` nil crash:** upstream stores the real `SendMailMailButton` in a loose global named `MailMailButton`, then replaces the Blizzard global with a proxy table. If the saved global is missing or another UI addon changes it, `SendMailFrame_CanSend()` and `sendmail_clear()` crash while calling `Enable()` / `Disable()`. The compatibility layer now preserves a strong reference to the real button before the proxy swap, restores it when the mailbox opens, replaces the fragile CanSend hook, and uses a guarded clear routine. +- **Paired subject edit-box hardening:** the same proxy pattern is used for `MailSubjectEditBox`; the real edit box is now preserved and restored alongside the send button to avoid the equivalent follow-up failure. + ## Remaining architectural risks upstream These are intentionally not rewritten in the compatibility layer because changing them would be much more invasive and could alter mail behavior: @@ -38,4 +44,4 @@ The compatibility build was tested in-game on Turtle WoW 1.18.x and the reported ## Status -**1.4.6 is the current stable release.** The fixes remain isolated in `TurtleMailFix.lua` so upstream code stays easy to compare and future upstream changes remain easier to merge. +**1.4.8 is the current stable release.** The fixes remain isolated in `TurtleMailFix.lua` so upstream code stays easy to compare and future upstream changes remain easier to merge. diff --git a/README.md b/README.md index 75f2417..7a750a4 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ When enabled, TurtleMail records sent and received mail information in its dedic ## 🔧 Turtle WoW compatibility -Version **1.4.7** extends the compatibility and safety layer for Turtle WoW 1.18.x environments. +Version **1.4.8** extends the compatibility and safety layer for Turtle WoW 1.18.x environments. The compatibility work is isolated in `TurtleMailFix.lua` so the original TurtleMail code remains easier to compare with upstream versions. @@ -95,6 +95,7 @@ The compatibility work is isolated in `TurtleMailFix.lua` so the original Turtle - Fixed calendar tooltip nil/stale-value cases. - Rebound the safer `OnUpdate` handler correctly after loading the compatibility layer. - Prevented the send queue from remaining stuck when an attachment becomes unavailable during sending. +- Fixed `MailMailButton` nil crashes while enabling/disabling or clearing the send form, and hardened the paired subject edit-box reference. The complete technical audit is available in [`AUDIT.md`](AUDIT.md). @@ -129,11 +130,11 @@ TurtleMail includes localization support for: Current stable version: -**1.4.7** +**1.4.8** Based on upstream **TurtleMail 1.4.5**. -Version 1.4.7 has been validated in-game on Turtle WoW 1.18.x without Lua errors in the tested setup. +Version 1.4.8 adds an additional safeguard for the legacy send-button proxy used by TurtleMail. The earlier compatibility fixes remain unchanged. ## 🖼️ Screenshots diff --git a/TurtleMail.toc b/TurtleMail.toc index d85a257..678103b 100644 --- a/TurtleMail.toc +++ b/TurtleMail.toc @@ -2,7 +2,7 @@ ## Interface: 11200 ## Title: |cffabd473Turtle|cffffffffMail ## Author: shirsig/sica, Dusk-92 (compatibility fixes) -## Version: 1.4.7 +## Version: 1.4.8 ## Notes: Mailbox enhancement - Turtle WoW 1.18.x compatibility ## SavedVariables: TurtleMail_AutoCompleteNames ## SavedVariablesPerCharacter: TurtleMail_To TurtleMail_Point TurtleMail_Log diff --git a/TurtleMailFix.lua b/TurtleMailFix.lua index 0396833..d48a172 100644 --- a/TurtleMailFix.lua +++ b/TurtleMailFix.lua @@ -8,7 +8,7 @@ local m = TurtleMail local getn = table.getn local function pack( ... ) return arg end -m.compat_version = "1.4.7" +m.compat_version = "1.4.8" local DEFAULT_SENT_FILTERS = { Money = 1, @@ -110,6 +110,75 @@ local function ensure_horizontal_bars() return m.api.MailHorizontalBarLeft and m.api.MailHorizontalBarRight end +-- Upstream keeps the real Send button and subject edit box in ad-hoc globals +-- (MailMailButton / MailSubjectEditBox), then replaces the Blizzard globals with +-- proxy tables. If another addon changes those globals, or sendmail_load() runs +-- with an already-proxied widget, the saved globals can become nil and later +-- calls crash. Preserve strong references before the upstream swap happens. +local preserved_send_mail_button = m.api.SendMailMailButton +local preserved_subject_edit_box = m.api.SendMailSubjectEditBox + +local function is_named_widget( widget, expected_name ) + if not widget or type( widget.GetName ) ~= "function" then return false end + return widget:GetName() == expected_name +end + +local function ensure_send_mail_button() + local button = preserved_send_mail_button + + if not is_named_widget( button, "SendMailMailButton" ) then + button = m.real_send_mail_button + end + if not is_named_widget( button, "SendMailMailButton" ) then + button = MailMailButton + end + if not is_named_widget( button, "SendMailMailButton" ) then + button = m.api.SendMailMailButton + end + + if is_named_widget( button, "SendMailMailButton" ) then + preserved_send_mail_button = button + m.real_send_mail_button = button + MailMailButton = button + return button + end + + return nil +end + +local function ensure_subject_edit_box() + local edit_box = preserved_subject_edit_box + + if not is_named_widget( edit_box, "SendMailSubjectEditBox" ) then + edit_box = m.real_subject_edit_box + end + if not is_named_widget( edit_box, "SendMailSubjectEditBox" ) then + edit_box = MailSubjectEditBox + end + if not is_named_widget( edit_box, "SendMailSubjectEditBox" ) then + edit_box = m.api.SendMailSubjectEditBox + end + + if is_named_widget( edit_box, "SendMailSubjectEditBox" ) then + preserved_subject_edit_box = edit_box + m.real_subject_edit_box = edit_box + MailSubjectEditBox = edit_box + return edit_box + end + + return nil +end + +local function restore_real_mail_widgets_for_upstream() + local button = ensure_send_mail_button() + local edit_box = ensure_subject_edit_box() + + -- Only restore the Blizzard globals immediately before upstream sendmail_load(). + -- That function intentionally replaces them with proxy tables afterwards. + if button then m.api.SendMailMailButton = button end + if edit_box then m.api.SendMailSubjectEditBox = edit_box end +end + -- SavedVariables are not guaranteed to have a valid schema. Repair them before -- the original handlers touch nested fields. do @@ -166,7 +235,50 @@ do local original = m.sendmail_load function m.sendmail_load() ensure_horizontal_bars() - if original then return original() end + restore_real_mail_widgets_for_upstream() + + if original then + local result = original() + -- Upstream has now installed its proxy globals. Re-assert the private + -- references used by the legacy MailMailButton/MailSubjectEditBox code. + ensure_send_mail_button() + ensure_subject_edit_box() + return result + end + end +end + +-- Replace the fragile upstream CanSend hook. The original directly calls the +-- global MailMailButton and crashes when that saved global is nil. +if m.hooks then + m.hook.SendMailFrame_CanSend = function() + local button = ensure_send_mail_button() + if not button then return end + + local name_box = m.api.SendMailNameEditBox + local send_money_button = m.api.SendMailSendMoneyButton + local send_money_frame = m.api.SendMailMoney + + local recipient = name_box and name_box.GetText and name_box:GetText() or "" + local attached = m.sendmail_num_attachments and m.sendmail_num_attachments() or 0 + local price = m.api.GetSendMailPrice and (tonumber( m.api.GetSendMailPrice() ) or 0) or 0 + local player_money = m.api.GetMoney and (tonumber( m.api.GetMoney() ) or 0) or 0 + local money = 0 + + if send_money_button and send_money_button.GetChecked and send_money_button:GetChecked() + and m.api.MoneyInputFrame_GetCopper and send_money_frame then + money = tonumber( m.api.MoneyInputFrame_GetCopper( send_money_frame ) ) or 0 + end + + local can_send = not m.sendmail_sending + and string.len( recipient ) > 0 + and money + price * math.max( 1, attached ) <= player_money + + if can_send then + button:Enable() + else + button:Disable() + end end end @@ -178,6 +290,67 @@ if m.hooks and m.hooks.SendMailFrame_Update then end end +-- Ensure the legacy click handler sees a valid subject edit box and real send +-- button before it reads them. This protects the same upstream proxy hack from +-- producing a follow-up MailSubjectEditBox nil error. +do + local original = m.send_mail_button_onclick + function m.send_mail_button_onclick() + ensure_send_mail_button() + ensure_subject_edit_box() + if original then return original() end + end +end + +-- Replace the upstream clear routine because it directly dereferences +-- MailMailButton and MailSubjectEditBox. Keep its behavior, but guard every UI +-- object so mailbox replacements cannot turn cleanup into another Lua error. +function m.sendmail_clear() + local any_item + + for i = 1, 21 do + local attachment = m.api[ "MailAttachment" .. i ] + if attachment then + any_item = any_item or attachment.item + attachment.item = nil + end + end + + if any_item and m.api.ClearCursor and m.api.PickupContainerItem then + m.api.ClearCursor() + m.api.PickupContainerItem( unpack( any_item ) ) + m.api.ClearCursor() + end + + local button = ensure_send_mail_button() + if button and button.Disable then button:Disable() end + + local name_box = m.api.SendMailNameEditBox + if name_box then + if name_box.SetText then name_box:SetText( "" ) end + if name_box.SetFocus then name_box:SetFocus() end + end + + local subject = ensure_subject_edit_box() + if subject and subject.SetText then subject:SetText( "" ) end + + if m.api.SendMailBodyEditBox and m.api.SendMailBodyEditBox.SetText then + m.api.SendMailBodyEditBox:SetText( "" ) + end + + if m.api.MoneyInputFrame_ResetMoney and m.api.SendMailMoney then + m.api.MoneyInputFrame_ResetMoney( m.api.SendMailMoney ) + end + + if m.api.SendMailRadioButton_OnClick then + m.api.SendMailRadioButton_OnClick( 1 ) + end + + if m.api.SendMailFrame_Update then + m.api.SendMailFrame_Update() + end +end + -- Do not assume MailFrame exists during bag events. function m.BAG_UPDATE() if m.api.MailFrame and m.api.MailFrame:IsVisible() and m.api.SendMailFrame_Update then @@ -187,6 +360,11 @@ end -- Safer MAIL_SHOW: package-button regions differ between UI replacements. function m.MAIL_SHOW() + -- Re-assert the legacy references whenever the mailbox opens. This also + -- recovers if another UI addon changed those globals after addon loading. + ensure_send_mail_button() + ensure_subject_edit_box() + if not m.api.MailFrame then return end if m.api.TurtleMail_Point then