5 Commits

Author SHA1 Message Date
Dusk-92 0bc6523fb2 Promote compatibility layer to 1.4.6 stable 2026-08-24 09:48:43 +02:00
Dusk-92 c166f65316 Update audit for stable 1.4.6 2026-08-24 09:47:57 +02:00
Dusk-92 bb7c47a4d6 Mark 1.4.6 as stable in README 2026-08-24 09:47:39 +02:00
Dusk-92 52e66fcf4b Release TurtleMail 1.4.6 2026-08-24 09:47:22 +02:00
Dusk-92 b22361f758 Turtle WoW 1.18.x compatibility fixes (1.4.6-compat.1)
Validated in-game without Lua errors. Adds compatibility and safety fixes for TurtleMail on Turtle WoW 1.18.x / Octo while keeping upstream code largely intact.
2026-08-24 09:44:29 +02:00
4 changed files with 559 additions and 8 deletions
+41
View File
@@ -0,0 +1,41 @@
# TurtleMail 1.4.5 — Compatibility audit
Target: Turtle WoW 1.18.x / Octo client.
## Fixed in 1.4.6
- **Horizontal mail bar crash:** the upstream code creates `MailHorizontalBarLeft/Right` and then assumes the textures are exported as globals. Some client/UI combinations return the textures without the expected global, causing `attempt to index field 'MailHorizontalBarLeft' (a nil value)`. The compatibility layer guarantees valid texture references before setup and updates.
- **Autocomplete SavedVariables crash:** `TurtleMail_AutoCompleteNames` and its realm/faction table are used without schema validation. The patch initializes/repairs missing or malformed SavedVariables before `ADDON_LOADED`, `PLAYER_LOGIN`, slash commands, and autocomplete.
- **Broken sender-learning hook:** upstream `GetInboxHeaderInfo` hook reads `arg[3]`/`arg[12]` from function inputs even though sender/canReply are return values. The patch captures return values first, so received senders can actually populate autocomplete.
- **Sent money not logged:** upstream checks the nonexistent `state.send_money` field. The patch records sent money when it is not COD and `sent_money > 0`.
- **Inbox bounds/COD safety:** header data is no longer queried/compared unsafely after the inbox index has moved past the last mail; nil COD values are treated as zero.
- **Stale inbox icons:** AH/returned icons are explicitly hidden on empty rows/pages.
- **Fragile package-button regions:** `MAIL_SHOW` no longer assumes regions 1 and 3 always exist.
- **Mailbox frame guards:** bag updates, attachment bookkeeping, inbox locking, and money display tolerate missing/replaced frames instead of immediately throwing Lua errors.
- **Locale-safe COD label:** unexpected `COD_AMOUNT` formatting no longer turns the label into nil and causes string concatenation errors.
- **Malformed log entries:** logging tolerates missing subjects/participant fields and repairs missing log tables.
- **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.
## 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:
1. TurtleMail directly replaces global Blizzard functions during `PLAYER_LOGIN` rather than using a cooperative hook system. This makes load order important when another addon also replaces the mail/bag functions.
2. `sendmail_load()` temporarily/proxy-replaces global `SendMailMailButton` and `SendMailSubjectEditBox`. This is a clever Vanilla-era workaround but can conflict with UI replacements.
3. Several layouts rely on specific Blizzard frame names and region ordering. The compatibility layer protects the confirmed/high-risk cases, but a full mail-frame replacement can still be incompatible.
4. The pfUI skin accesses pfUI internals (`MailFrame.backdrop`, `pfUI_config`, specific widget members). A future pfUI layout change can therefore break the skin even when core TurtleMail remains functional.
5. Inbox polling is frame-count based (`200` OnUpdate ticks), so the interval changes with FPS. It is not a correctness bug but is less predictable than elapsed-time polling.
6. The addon has no automated runtime test harness for Turtle WoW mail APIs; future changes still need in-game validation with representative mail/send scenarios.
## Compatibility context
The upstream repository already had a Turtle WoW 1.18.1 report where `MailFrame` became nil; that report was ultimately traced to MoveAnything. This reinforces that TurtleMail is sensitive to addons that replace or re-parent the mail UI.
## Validation
The compatibility build was tested in-game on Turtle WoW 1.18.x and the reported setup produced no Lua errors after the fixes were applied. Because mailbox behavior can be affected by other UI addons, future changes should still be checked with normal mail, item mail, gold, COD, AH/returned mail, multi-attachment send, autocomplete, logging, and pfUI when applicable.
## 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.
+36 -5
View File
@@ -1,6 +1,13 @@
# TurtleMail - WoW 1.12 addOn
# TurtleMail - WoW 1.12 / Turtle WoW
This fork keeps the original TurtleMail features while adding a compatibility and safety layer for **Turtle WoW 1.18.x / Octo**.
> Stable release: **1.4.6**
> Based on upstream **sica42/TurtleMail 1.4.5**.
> The compatibility work is isolated in `TurtleMailFix.lua` so upstream changes remain easy to compare and merge.
## Features
An extension to the Blizzard mail interface which
- **Automatically opens mail, very rapidly**
- **Mails multiple items at once, very rapidly**
- **Autocompletes recipient names**
@@ -15,8 +22,32 @@ An extension to the Blizzard mail interface which
Note that COD is always ignored when opening, both automatically as well as by **\<Right Click>**.
Logging is disabled by default. Enable with `/tm log` command
Logging is disabled by default. Enable with `/tm log`.
![Alt text](https://i.imgur.com/H0MUmXd.png)
## Turtle WoW compatibility fixes
![Alt text](https://i.imgur.com/LM7tRcx.png)
Version **1.4.6** addresses:
- `MailHorizontalBarLeft` / `MailHorizontalBarRight` nil crashes.
- Missing or malformed `TurtleMail_AutoCompleteNames` SavedVariables.
- Broken learning of sender names for autocomplete.
- Sent-money logging not being recorded correctly.
- Inbox index/COD nil safety during automatic opening.
- Stale AH/returned icons on empty inbox rows.
- Fragile mailbox/package-frame lookups.
- A calendar tooltip nil/stale-value edge case.
- Several additional guards around mailbox UI replacements and malformed state.
See [`AUDIT.md`](AUDIT.md) for the full audit and remaining architectural risks.
## Validation status
Version **1.4.6** has been promoted to stable after in-game testing on Turtle WoW 1.18.x completed without Lua errors in the tested setup.
The patch intentionally keeps the original send/open workflow instead of redesigning it. Addons that heavily replace or re-parent the Blizzard mail UI can still create compatibility issues; known architectural risks are documented in `AUDIT.md`.
## Screenshots
![TurtleMail](https://i.imgur.com/H0MUmXd.png)
![TurtleMail](https://i.imgur.com/LM7tRcx.png)
+4 -3
View File
@@ -1,10 +1,11 @@
## Interface: 11200
## Title: |cffabd473Turtle|cffffffffMail
## Author: shirsig/sica
## Version: 1.4.5
## Notes: Mailbox enhancement
## Author: shirsig/sica, Dusk-92 (compatibility fixes)
## Version: 1.4.6
## Notes: Mailbox enhancement - Turtle WoW 1.18.x compatibility
## SavedVariables: TurtleMail_AutoCompleteNames
## SavedVariablesPerCharacter: TurtleMail_To TurtleMail_Point TurtleMail_Log
TurtleMail.xml
TurtleMailFix.lua
+478
View File
@@ -0,0 +1,478 @@
-- TurtleMailFix.lua
-- Compatibility/safety layer for TurtleMail 1.4.5 on Turtle WoW 1.18.x / Octo.
-- Loaded after TurtleMail.xml so it can replace fragile handlers before ADDON_LOADED/PLAYER_LOGIN.
if not TurtleMail then return end
local m = TurtleMail
local getn = table.getn
local function pack( ... ) return arg end
m.compat_version = "1.4.6"
local DEFAULT_SENT_FILTERS = {
Money = 1,
COD = 1,
Other = 1,
}
local DEFAULT_RECEIVED_FILTERS = {
Money = 1,
COD = 1,
Other = 1,
Returned = 1,
AH = 1,
AHSold = 1,
AHOutbid = 1,
AHWon = 1,
AHCancelled = 1,
AHExpired = 1,
}
local INBOX_AUCTIONHOUSES = {
[ "Stormwind Auction House" ] = true,
[ "Alliance Auction House" ] = true,
[ "Darnassus Auction House" ] = true,
[ "Undercity Auction House" ] = true,
[ "Thunder Bluff Auction House" ] = true,
[ "Horde Auction House" ] = true,
[ "Blackwater Auction House" ] = true,
}
local function merge_defaults( target, defaults )
for key, value in pairs( defaults ) do
if target[ key ] == nil then
target[ key ] = value
end
end
end
local function ensure_saved_variables()
if type( m.api.TurtleMail_Log ) ~= "table" then
m.api.TurtleMail_Log = {}
end
local log = m.api.TurtleMail_Log
if type( log.Sent ) ~= "table" then log.Sent = {} end
if type( log.Received ) ~= "table" then log.Received = {} end
if type( log.Settings ) ~= "table" then log.Settings = {} end
local settings = log.Settings
if settings.Enabled == nil then settings.Enabled = false end
if type( settings.SentFilters ) ~= "table" then settings.SentFilters = {} end
if type( settings.ReceivedFilters ) ~= "table" then settings.ReceivedFilters = {} end
merge_defaults( settings.SentFilters, DEFAULT_SENT_FILTERS )
merge_defaults( settings.ReceivedFilters, DEFAULT_RECEIVED_FILTERS )
if type( m.api.TurtleMail_AutoCompleteNames ) ~= "table" then
m.api.TurtleMail_AutoCompleteNames = {}
end
end
local function autocomplete_key()
local realm = m.api.GetCVar and m.api.GetCVar( "realmName" ) or "UnknownRealm"
local faction = m.api.UnitFactionGroup and m.api.UnitFactionGroup( "player" ) or "UnknownFaction"
return tostring( realm or "UnknownRealm" ) .. "|" .. tostring( faction or "UnknownFaction" )
end
local function ensure_autocomplete_table()
ensure_saved_variables()
local key = autocomplete_key()
if type( m.api.TurtleMail_AutoCompleteNames[ key ] ) ~= "table" then
m.api.TurtleMail_AutoCompleteNames[ key ] = {}
end
return key, m.api.TurtleMail_AutoCompleteNames[ key ]
end
local function sanitize_autocomplete_table()
local _, names = ensure_autocomplete_table()
for name, last_seen in pairs( names ) do
if type( name ) ~= "string" or type( last_seen ) ~= "number" then
names[ name ] = nil
end
end
end
-- Turtle WoW/Octo can return a texture from CreateTexture without exposing it
-- through the expected global name. The original addon assumes the global exists.
local function ensure_horizontal_bars()
local frame = m.api.SendMailFrame
if not frame or not frame.CreateTexture then return false end
if not m.api.MailHorizontalBarLeft then
m.api.MailHorizontalBarLeft = frame:CreateTexture( nil, "BACKGROUND" )
end
if not m.api.MailHorizontalBarRight then
m.api.MailHorizontalBarRight = frame:CreateTexture( nil, "BACKGROUND" )
end
return m.api.MailHorizontalBarLeft and m.api.MailHorizontalBarRight
end
-- SavedVariables are not guaranteed to have a valid schema. Repair them before
-- the original handlers touch nested fields.
do
local original = m.ADDON_LOADED
function m.ADDON_LOADED()
if arg1 == "TurtleMail" then
ensure_saved_variables()
end
if original then original() end
if arg1 == "TurtleMail" and m.info then
m.info( "Compatibility fixes loaded (|cffeda55f" .. m.compat_version .. "|r)." )
end
end
end
do
local original = m.PLAYER_LOGIN
function m.PLAYER_LOGIN()
ensure_saved_variables()
sanitize_autocomplete_table()
if original then original() end
end
end
do
local original = m.slash_command
function m.slash_command( args )
ensure_saved_variables()
if original then return original( args ) end
end
end
-- Make autocomplete resilient to missing/corrupt SavedVariables.
do
local original = m.add_auto_complete_name
function m.add_auto_complete_name( name )
if type( name ) ~= "string" or name == "" then return end
ensure_autocomplete_table()
if original then return original( name ) end
end
end
if GetSuggestions then
local original = GetSuggestions
function GetSuggestions()
ensure_autocomplete_table()
return original()
end
end
-- Ensure the two horizontal bar textures exist before the original setup/update
-- routines try to address them by global name.
do
local original = m.sendmail_load
function m.sendmail_load()
ensure_horizontal_bars()
if original then return original() end
end
end
if m.hooks and m.hooks.SendMailFrame_Update then
local original = m.hooks.SendMailFrame_Update
m.hook.SendMailFrame_Update = function( ... )
ensure_horizontal_bars()
return original( unpack( arg ) )
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
m.api.SendMailFrame_Update()
end
end
-- Safer MAIL_SHOW: package-button regions differ between UI replacements.
function m.MAIL_SHOW()
if not m.api.MailFrame then return end
if m.api.TurtleMail_Point then
m.debug( "Set point" )
m.api.MailFrame:SetPoint( m.api.TurtleMail_Point.point, m.api.TurtleMail_Point.x, m.api.TurtleMail_Point.y )
end
if not m.first_show then
m.first_show = true
local package = m.api.SendMailPackageButton
if package then
if m.pfui_skin_enabled and m.api.pfUI and m.api.pfUI.api and m.api.pfUI.api.StripTextures then
m.api.pfUI.api.StripTextures( package )
end
local regions = { package:GetRegions() }
if regions[ 1 ] and regions[ 1 ].Hide then regions[ 1 ]:Hide() end
if regions[ 3 ] and regions[ 3 ].Hide then regions[ 3 ]:Hide() end
package:Disable()
package:SetScript( "OnReceiveDrag", nil )
package:SetScript( "OnDragStart", nil )
end
end
if m.api.MailFrameTab3 then
if m.log_enabled then
m.api.MailFrameTab3:Show()
else
m.api.MailFrameTab3:Hide()
end
end
m.timer = 0
m.money_received = 0
m.update_money( 0 )
end
-- Check bounds before querying inbox headers and tolerate nil COD values.
function m.on_update()
if not m.api.MailFrame or not m.api.MailFrame:IsVisible() then return end
if m._cursorItem then
m.debug( "on_update: cursorItem" )
m.cursorItem = m._cursorItem
m._cursorItem = nil
end
if m.sendmail_update then
m.debug( "on_update: sendmail" )
m.sendmail_update = nil
if m.sendmail_sending then
m.debug( "m.sendmail_sending" )
m.sendmail_send()
end
end
if m.inbox_update then
m.debug( "on_update: inbox_update" )
m.inbox_update = false
local index = tonumber( m.inbox_index ) or 1
local total = m.api.GetInboxNumItems and (m.api.GetInboxNumItems() or 0) or 0
if index > total then
if (tonumber( m.money_received ) or 0) > 0 then
m.info( string.format( "%s%s.", m.format_money( m.money_received ), L[ "collected" ] ) )
end
m.inbox_abort()
else
local _, _, _, _, _, cod, _, _, _, _, _, _, isGM = m.api.GetInboxHeaderInfo( index )
cod = tonumber( cod ) or 0
if m.inbox_skip or cod > 0 or isGM then
m.inbox_skip = false
m.inbox_index = index + 1
m.inbox_update = true
else
m.inbox_open( index )
end
end
end
if type( m.timer ) ~= "number" then m.timer = 0 end
if m.timer > 0 then
m.timer = m.timer - 1
elseif not m.inbox_opening and m.api.CheckInbox then
m.timer = 200
m.api.CheckInbox()
end
end
-- init() bound the original on_update function before this compatibility file was
-- loaded. Rebind the frame script so the safer implementation above is actually used.
if m.update_frame and m.update_frame.SetScript then
m.update_frame:SetScript( "OnUpdate", m.on_update )
end
-- Hide stale AH/returned icons on rows that no longer contain mail.
function m.MAIL_INBOX_UPDATE()
if m.inbox_opening then
m.inbox_update = true
end
if not m.api.InboxFrame then return end
local page = tonumber( m.api.InboxFrame.pageNum ) or 1
local total = m.api.GetInboxNumItems and (m.api.GetInboxNumItems() or 0) or 0
for i = 1, 7 do
local index = i + (page - 1) * 7
local auction_icon = m.api[ "TurtleMailAuctionIcon" .. i ]
local returned_icon = m.api[ "TurtleMailReturnedArrow" .. i ]
if index <= total then
local _, _, sender, _, _, _, _, _, _, was_returned = m.api.GetInboxHeaderInfo( index )
if auction_icon then
if INBOX_AUCTIONHOUSES[ sender ] then auction_icon:Show() else auction_icon:Hide() end
end
if returned_icon then
if was_returned then returned_icon:Show() else returned_icon:Hide() end
end
else
if auction_icon then auction_icon:Hide() end
if returned_icon then returned_icon:Hide() end
end
end
end
-- Guard frame lookups used while the mailbox is opening/closing.
function m.inbox_update_lock()
for i = 1, 7 do
local icon = m.api[ "MailItem" .. i .. "ButtonIcon" ]
local button = m.api[ "MailItem" .. i .. "Button" ]
if icon and icon.SetDesaturated then
icon:SetDesaturated( m.inbox_opening )
end
if m.inbox_opening and button and button.SetChecked then
button:SetChecked( nil )
end
end
end
-- Locale-safe COD label.
function m.set_cod_text()
local cod_amount = m.api.COD_AMOUNT or "COD:"
local text = string.sub( cod_amount, 1, math.max( 0, string.len( cod_amount ) - 1 ) )
if not m.pfui_skin_enabled then
text = string.match( text, "^(.-)%s+%S+$" ) or text
end
if not m.api.SendMailMoneyText then return end
if m.api.SendMailCODAllButton and m.api.SendMailCODAllButton:GetChecked() then
m.api.SendMailMoneyText:SetText( text .. " " .. L[ "each mail" ] .. ":" )
else
m.api.SendMailMoneyText:SetText( text .. " " .. L[ "1st mail" ] .. ":" )
end
end
function m.update_money( money )
m.money_received = (tonumber( m.money_received ) or 0) + (tonumber( money ) or 0)
if not m.api.MoneyReceived then return end
m.api.MoneyReceived:SetText( L[ "Money received" ] .. ": " .. m.format_money( m.money_received ) )
if m.money_received > 0 then
m.api.MoneyReceived:Show()
else
m.api.MoneyReceived:Hide()
end
end
-- The original hook reads arg[3]/arg[12] from the function INPUT arguments.
-- GetInboxHeaderInfo receives an inbox index; sender/canReply are RETURN values.
-- Capture the return list first so sender autocomplete actually works.
if m.hooks and m.hooks.GetInboxHeaderInfo then
m.hook.GetInboxHeaderInfo = function( ... )
if not m.orig.GetInboxHeaderInfo then return end
local ret = pack( m.orig.GetInboxHeaderInfo( unpack( arg ) ) )
local sender = ret[ 3 ]
local canReply = ret[ 12 ]
if sender and canReply then
m.add_auto_complete_name( sender )
end
return unpack( ret )
end
end
-- Guard MailFrame lookup in attachment bookkeeping.
do
local original = m.sendmail_attached
function m.sendmail_attached( bag, slot )
if not m.api.MailFrame or not m.api.MailFrame:IsVisible() then return false end
if original then return original( bag, slot ) end
return false
end
end
-- Fix sent-money logging and tolerate malformed/system mail subjects.
function m.log.add( log_type, state )
if not m.log_enabled or type( state ) ~= "table" then return end
ensure_saved_variables()
m.debug( "Logging " .. tostring( log_type ) .. " message" )
local data = {
timestamp = time(),
icon = state.icon,
item = state.item,
}
local cod = tonumber( state.cod )
if cod and cod > 0 then data.cod = cod end
if log_type == "Sent" then
data.participant = state.to or ""
data.subject = state.sent_subject or state.subject or ""
local sent_money = tonumber( state.sent_money ) or 0
if not data.cod and sent_money > 0 then
data.money = sent_money
end
else
data.participant = state.from or ""
data.subject = state.subject or ""
data.returned = state.returned
data.gm = state.gm
local received_money = tonumber( state.money ) or 0
if received_money > 0 then data.money = received_money end
local function subject_matches( pattern )
if type( pattern ) ~= "string" or pattern == "" or data.subject == "" then return false end
local needle = string.gsub( pattern, "%%s", "" )
return needle ~= "" and string.find( data.subject, needle, 1, true ) ~= nil
end
if subject_matches( m.api.AUCTION_SOLD_MAIL_SUBJECT ) then
data.ah = "Sold"
elseif subject_matches( m.api.AUCTION_REMOVED_MAIL_SUBJECT ) then
data.ah = "Removed"
elseif subject_matches( m.api.AUCTION_EXPIRED_MAIL_SUBJECT ) then
data.ah = "Expired"
elseif subject_matches( m.api.AUCTION_WON_MAIL_SUBJECT ) then
data.ah = "Won"
elseif subject_matches( m.api.AUCTION_OUTBID_MAIL_SUBJECT ) then
data.ah = "Outbid"
end
end
if type( m.api.TurtleMail_Log[ log_type ] ) ~= "table" then
m.api.TurtleMail_Log[ log_type ] = {}
end
table.insert( m.api.TurtleMail_Log[ log_type ], data )
end
-- Calendar day buttons can retain an old .mails value or have no value at all.
-- Replace only the tooltip handler; calendar selection/layout remains upstream.
if m.calendar and m.calendar.show then
local original_show = m.calendar.show
local function calendar_on_enter( button )
return function()
local enabled = true
if button.IsEnabled then enabled = button:IsEnabled() end
local mails = tonumber( button.mails ) or 0
if enabled and mails > 0 then
m.api.GameTooltip:SetOwner( button, "ANCHOR_RIGHT" )
m.api.GameTooltip:SetText( mails .. " mail" .. (mails > 1 and "s" or ""), 1, 1, 1, 1, true )
m.api.GameTooltip:Show()
else
m.api.GameTooltip:Hide()
end
end
end
m.calendar.show = function( data, current_date, anchor, on_select )
if type( data ) ~= "table" then data = {} end
original_show( data, current_date or time(), anchor, on_select )
for i = 1, 42 do
local button = m.api[ "TurtleMailCalendarDay" .. i .. "Button" ]
if button then
button:SetScript( "OnEnter", calendar_on_enter( button ) )
end
end
end
end