|
|
|
@@ -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.12"
|
|
|
|
|
|
|
|
|
|
local DEFAULT_SENT_FILTERS = {
|
|
|
|
|
Money = 1,
|
|
|
|
@@ -110,6 +110,190 @@ 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
|
|
|
|
|
|
|
|
|
|
-- The upstream Open Mail button is created only once in inbox_load(). If that
|
|
|
|
|
-- creation is skipped/interrupted, or another UI addon hides/re-parents the
|
|
|
|
|
-- button afterwards, TurtleMail never recreates it. Keep the button recoverable,
|
|
|
|
|
-- but only mutate its layout when something is actually wrong. This avoids doing
|
|
|
|
|
-- repeated SetParent/ClearAllPoints/SetPoint work during inbox refresh bursts.
|
|
|
|
|
local function ensure_open_mail_button()
|
|
|
|
|
local inbox = m.api.InboxFrame
|
|
|
|
|
if not inbox or not m.api.CreateFrame then return nil end
|
|
|
|
|
|
|
|
|
|
local button = m.api.TurtleMailOpenMailButton
|
|
|
|
|
local created = false
|
|
|
|
|
if not button or type( button.SetPoint ) ~= "function" then
|
|
|
|
|
button = m.api.CreateFrame( "Button", "TurtleMailOpenMailButton", inbox, "UIPanelButtonTemplate" )
|
|
|
|
|
m.api.TurtleMailOpenMailButton = button
|
|
|
|
|
created = true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local parent_changed = false
|
|
|
|
|
if button.GetParent and button.SetParent then
|
|
|
|
|
if button:GetParent() ~= inbox then
|
|
|
|
|
button:SetParent( inbox )
|
|
|
|
|
parent_changed = true
|
|
|
|
|
end
|
|
|
|
|
elseif created and button.SetParent then
|
|
|
|
|
button:SetParent( inbox )
|
|
|
|
|
parent_changed = true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local needs_anchor = created or parent_changed
|
|
|
|
|
if not needs_anchor and button.GetPoint then
|
|
|
|
|
local point, relative_to, relative_point, x, y = button:GetPoint()
|
|
|
|
|
needs_anchor = point ~= "BOTTOM"
|
|
|
|
|
or (relative_to and relative_to ~= inbox)
|
|
|
|
|
or relative_point ~= "BOTTOM"
|
|
|
|
|
or x ~= -10
|
|
|
|
|
or y ~= 90
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if needs_anchor then
|
|
|
|
|
if button.ClearAllPoints then button:ClearAllPoints() end
|
|
|
|
|
button:SetPoint( "BOTTOM", inbox, "BOTTOM", -10, 90 )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local wanted_text = m.api.OPENMAIL or "Open Mail"
|
|
|
|
|
local text_changed = created
|
|
|
|
|
if button.GetText then
|
|
|
|
|
text_changed = button:GetText() ~= wanted_text
|
|
|
|
|
end
|
|
|
|
|
if text_changed and button.SetText then
|
|
|
|
|
button:SetText( wanted_text )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Recalculate dimensions only when the button was created or its label changed.
|
|
|
|
|
if created or text_changed then
|
|
|
|
|
local width = 120
|
|
|
|
|
local font_string = button.GetFontString and button:GetFontString()
|
|
|
|
|
if font_string and font_string.GetStringWidth then
|
|
|
|
|
width = math.max( 120, 30 + (font_string:GetStringWidth() or 0) )
|
|
|
|
|
end
|
|
|
|
|
if button.SetWidth then button:SetWidth( width ) end
|
|
|
|
|
if button.SetHeight then button:SetHeight( 25 ) end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if button.SetScript then
|
|
|
|
|
local onclick = button.GetScript and button:GetScript( "OnClick" ) or nil
|
|
|
|
|
if created or onclick ~= m.inbox_open_all then
|
|
|
|
|
button:SetScript( "OnClick", m.inbox_open_all )
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if inbox.GetFrameLevel and button.SetFrameLevel then
|
|
|
|
|
local target_level = (inbox:GetFrameLevel() or 0) + 5
|
|
|
|
|
local current_level = button.GetFrameLevel and button:GetFrameLevel() or nil
|
|
|
|
|
if current_level ~= target_level then
|
|
|
|
|
button:SetFrameLevel( target_level )
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if button.Enable then
|
|
|
|
|
if not button.IsEnabled or not button:IsEnabled() then
|
|
|
|
|
button:Enable()
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
if button.Show then
|
|
|
|
|
if not button.IsShown or not button:IsShown() then
|
|
|
|
|
button:Show()
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return button
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Replace the fragile one-shot inbox loader with an idempotent version. This
|
|
|
|
|
-- preserves upstream behavior while making Open Mail recoverable instead of
|
|
|
|
|
-- relying on the initial CreateFrame call succeeding forever.
|
|
|
|
|
function m.inbox_load()
|
|
|
|
|
if m.api.InboxFrame and m.api.InboxFrame.EnableMouse then
|
|
|
|
|
m.api.InboxFrame:EnableMouse( false )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
ensure_open_mail_button()
|
|
|
|
|
|
|
|
|
|
for i = 1, 7 do
|
|
|
|
|
local auction_texture = m.api[ "TurtleMailAuctionIcon" .. i .. "Texture" ]
|
|
|
|
|
local returned_texture = m.api[ "TurtleMailReturnedArrow" .. i .. "Texture" ]
|
|
|
|
|
local color = m.api.NORMAL_FONT_COLOR
|
|
|
|
|
|
|
|
|
|
if color and auction_texture and auction_texture.SetVertexColor then
|
|
|
|
|
auction_texture:SetVertexColor( color.r, color.g, color.b )
|
|
|
|
|
end
|
|
|
|
|
if color and returned_texture and returned_texture.SetVertexColor then
|
|
|
|
|
returned_texture:SetVertexColor( color.r, color.g, color.b )
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- SavedVariables are not guaranteed to have a valid schema. Repair them before
|
|
|
|
|
-- the original handlers touch nested fields.
|
|
|
|
|
do
|
|
|
|
@@ -166,7 +350,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 +405,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 +475,23 @@ end
|
|
|
|
|
|
|
|
|
|
-- Safer MAIL_SHOW: package-button regions differ between UI replacements.
|
|
|
|
|
function m.MAIL_SHOW()
|
|
|
|
|
-- Re-assert the legacy send-widget references whenever the mailbox opens.
|
|
|
|
|
-- Open Mail follows the lighter Otari-style path: when the existing button
|
|
|
|
|
-- is still shown and parented to InboxFrame, do nothing at all. Only invoke
|
|
|
|
|
-- the full recovery routine if the button is missing, hidden, or re-parented.
|
|
|
|
|
ensure_send_mail_button()
|
|
|
|
|
ensure_subject_edit_box()
|
|
|
|
|
|
|
|
|
|
local open_mail_button = m.api.TurtleMailOpenMailButton
|
|
|
|
|
local open_mail_ok = open_mail_button
|
|
|
|
|
and type( open_mail_button.SetPoint ) == "function"
|
|
|
|
|
and (not open_mail_button.GetParent or open_mail_button:GetParent() == m.api.InboxFrame)
|
|
|
|
|
and (not open_mail_button.IsShown or open_mail_button:IsShown())
|
|
|
|
|
|
|
|
|
|
if not open_mail_ok then
|
|
|
|
|
ensure_open_mail_button()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if not m.api.MailFrame then return end
|
|
|
|
|
|
|
|
|
|
if m.api.TurtleMail_Point then
|
|
|
|
@@ -616,3 +921,83 @@ do
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Performance compatibility fix inspired by Otari98/TurtleMail: the upstream
|
|
|
|
|
-- GetContainerItemInfo hook builds a temporary table with pack()/unpack() on
|
|
|
|
|
-- every bag query. Bag and mailbox refreshes can call this hook very frequently,
|
|
|
|
|
-- creating avoidable garbage and occasional GC hitches. Detect the native return
|
|
|
|
|
-- count once after the normal PLAYER_LOGIN hook setup, then use a fixed-local
|
|
|
|
|
-- wrapper with no per-call temporary table. Exact return arity is preserved for
|
|
|
|
|
-- up to 12 values so extended/custom clients remain compatible; if detection is
|
|
|
|
|
-- inconclusive, the existing upstream hook is left untouched.
|
|
|
|
|
do
|
|
|
|
|
local original_player_login = m.PLAYER_LOGIN
|
|
|
|
|
local legacy_hook = m.hooks and m.hooks.GetContainerItemInfo or nil
|
|
|
|
|
local detected_return_count
|
|
|
|
|
|
|
|
|
|
local function count_returns( ... )
|
|
|
|
|
return arg.n or getn( arg )
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function detect_container_info_return_count()
|
|
|
|
|
local fn = m.orig and m.orig.GetContainerItemInfo
|
|
|
|
|
if type( fn ) ~= "function" then return nil end
|
|
|
|
|
|
|
|
|
|
-- Prefer an occupied inventory slot so clients that return no values for an
|
|
|
|
|
-- empty slot cannot make us detect the wrong arity.
|
|
|
|
|
if m.api.GetContainerNumSlots then
|
|
|
|
|
for bag = 0, 4 do
|
|
|
|
|
local slots = tonumber( m.api.GetContainerNumSlots( bag ) ) or 0
|
|
|
|
|
for slot = 1, slots do
|
|
|
|
|
local texture = fn( bag, slot )
|
|
|
|
|
if texture then
|
|
|
|
|
return count_returns( fn( bag, slot ) )
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- A normal character should always have at least one item, but retain a
|
|
|
|
|
-- conservative fallback for unusual test clients or empty inventories.
|
|
|
|
|
local count = count_returns( fn( 0, 1 ) )
|
|
|
|
|
if count and count > 0 then return count end
|
|
|
|
|
return nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function optimized_get_container_item_info( bag, slot )
|
|
|
|
|
local r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12 =
|
|
|
|
|
m.orig.GetContainerItemInfo( bag, slot )
|
|
|
|
|
|
|
|
|
|
if not r3 and m.sendmail_attached( bag, slot ) then
|
|
|
|
|
r3 = 1
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if detected_return_count == 1 then return r1 end
|
|
|
|
|
if detected_return_count == 2 then return r1, r2 end
|
|
|
|
|
if detected_return_count == 3 then return r1, r2, r3 end
|
|
|
|
|
if detected_return_count == 4 then return r1, r2, r3, r4 end
|
|
|
|
|
if detected_return_count == 5 then return r1, r2, r3, r4, r5 end
|
|
|
|
|
if detected_return_count == 6 then return r1, r2, r3, r4, r5, r6 end
|
|
|
|
|
if detected_return_count == 7 then return r1, r2, r3, r4, r5, r6, r7 end
|
|
|
|
|
if detected_return_count == 8 then return r1, r2, r3, r4, r5, r6, r7, r8 end
|
|
|
|
|
if detected_return_count == 9 then return r1, r2, r3, r4, r5, r6, r7, r8, r9 end
|
|
|
|
|
if detected_return_count == 10 then return r1, r2, r3, r4, r5, r6, r7, r8, r9, r10 end
|
|
|
|
|
if detected_return_count == 11 then return r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11 end
|
|
|
|
|
if detected_return_count == 12 then return r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12 end
|
|
|
|
|
|
|
|
|
|
-- Should not normally be reached; keep the previous behavior if a custom
|
|
|
|
|
-- client exposes an unexpected signature.
|
|
|
|
|
if legacy_hook then return legacy_hook( bag, slot ) end
|
|
|
|
|
return r1, r2, r3, r4, r5
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function m.PLAYER_LOGIN()
|
|
|
|
|
if original_player_login then original_player_login() end
|
|
|
|
|
|
|
|
|
|
detected_return_count = detect_container_info_return_count()
|
|
|
|
|
if detected_return_count and detected_return_count >= 1 and detected_return_count <= 12 then
|
|
|
|
|
m.api.GetContainerItemInfo = optimized_get_container_item_info
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|