X3D Mailbox (โค้ดรับของ)
กล่องจดหมายออฟไลน์พร้อมของแนบ + โค้ดรับของ /redeem ฟรี ไม่ต้องลงฐานข้อมูล — ส่งไอเทมและเงินให้ผู้เล่นที่ออฟไลน์ แยกแท็บจดหมาย/ตู้นิรภัย ต่อได้ทุกเฟรมเวิร์ก
ซื้อ / รับฟรีตั้งค่า configการติดตั้ง
- แตกไฟล์แล้ว เปลี่ยนชื่อโฟลเดอร์เป็น
x3d_mailbox(สคริปต์อื่นเรียก export ด้วยชื่อนี้) - วางใน
resources/ - เพิ่มใน
server.cfg:
ensure x3d_mailbox
เสร็จแล้ว — เก็บข้อมูลเป็นไฟล์ data/mailbox.json ในตัว resource เอง ไม่ต้องรัน SQL ใด ๆ
ผู้เล่นเปิดกล่องจดหมายด้วย /mailbox หรือปุ่ม F7 (Config.Key, ว่าง = ไม่ผูกปุ่ม)
ในพาเนล: สองแท็บ จดหมาย (หมดอายุได้) กับ ตู้นิรภัย (ช่องที่สอง แยกนับ unread/รับทั้งหมดกันคนละกล่อง — ใช้ส่งของจากโค้ดรับของได้), กดรับทีละฉบับหรือ รับทั้งหมด ในคลิกเดียว (เฉพาะแท็บที่เปิดอยู่), ลบ จดหมายที่รับของแล้ว, นับถอยหลังเวลาหมดอายุ, แลกโค้ดผ่านช่องกรอกในพาเนล และเด้งแจ้งเตือนทันทีถ้าผู้เล่นออนไลน์อยู่ตอนได้จดหมายใหม่
ไม่ต้องใช้ฐานข้อมูล ไม่ต้องใช้เฟรมเวิร์ก ไม่มีไลเซนส์ · ทำงานกับ ESX ได้ทันที และทำงานแบบ standalone ได้ถ้าไม่มีเฟรมเวิร์ก
ต่อกับเฟรมเวิร์ก — 4 adapter ใน addons/
| โฟลเดอร์ | หน้าที่ |
|---|---|
addons/framework/ |
Framework.IsBusy() (client) — เปิดกล่องจดหมายตอนนี้ไม่ได้ไหม (ตาย/pause menu); Framework.Identifier(src) / Framework.IsAdmin(src) (server) — คีย์ถาวรของผู้เล่นและสิทธิ์แอดมิน |
addons/notify/ |
Notify(message, kind) (client) — การแจ้งเตือน |
addons/reward/ |
Reward.CanCarry / Reward.GiveItem / Reward.GiveMoney (server) — ระบบไอเทม/เงิน |
addons/storage/ |
Storage.Get/Set/All/Flush (server) — ที่เก็บข้อมูล (ดูหัวข้อถัดไป) |
ทุกไฟล์มีค่าเริ่มต้นที่ใช้กับ ESX ได้ทันที (เช็ก GetResourceState('es_extended') เอง) และมีตัวอย่าง QBCore / ox_lib คอมเมนต์ไว้ท้ายไฟล์ให้คัดลอกทับ ไม่มีเฟรมเวิร์กเลยก็ยังเปิดกล่องจดหมายได้ แค่รับของแนบไม่ได้จนกว่าจะต่อ Reward.GiveItem/GiveMoney เอง (มี print เตือนใน console ถ้ายังไม่ได้ต่อ)
เก็บข้อมูล — JSON ค่าเริ่มต้น หรือสลับเป็น MySQL
ค่าเริ่มต้น: ไฟล์ JSON เดี่ยว (Config.Storage.File, ค่าเริ่มต้น data/mailbox.json) โหลดครั้งเดียวตอน resource เริ่ม เขียนกลับทุก Config.Storage.FlushSeconds วินาที (ค่าเริ่มต้น 30) และตอนผู้เล่นออก/resource หยุด ไฟล์เสียอ่านไม่ได้ → ย้ายไปเก็บเป็น data/mailbox.broken.json แล้วเริ่มไฟล์ใหม่ ไม่ทับทิ้งของเดิม
อยากใช้ MySQL แทน:
- ตั้ง
Config.Storage.Driver = 'oxmysql'— ตารางx3d_mailboxถูกสร้างให้อัตโนมัติ ไม่ต้อง include@oxmysql - รีสตาร์ท resource เป็นอันเสร็จ (จะแก้ 4 ฟังก์ชัน
Storage.*ในaddons/storage/server.luaก็ต่อเมื่อต้องการที่เก็บข้อมูลแบบเขียนเอง)
Exports
-- target = server id (number) หรือ identifier (string) ก็ได้
exports['x3d_mailbox']:SendMail(target, {
title = 'ของรางวัลกิจกรรม',
body = 'ขอบคุณที่เข้าร่วม',
sender = 'ทีมงาน', -- ไม่ใส่ = ใช้ชื่อกล่องจดหมาย
box = 'mail', -- 'mail' (ค่าเริ่มต้น, หมดอายุได้) หรือ 'safe' (ตู้ที่สอง)
expires = os.time() + 86400, -- unix timestamp, ไม่ใส่ = ไม่หมดอายุ
items = { { item = 'bread', count = 5 } },
money = { { account = 'money', amount = 500 } },
})
exports['x3d_mailbox']:SendMailAll({ title = 'ประกาศ', body = 'เซิร์ฟเวอร์จะปิดปรับปรุงคืนนี้' })
exports['x3d_mailbox']:GetMails(src) --> { {id, title, body, sender, at, expires, read, claimed, box, items, money}, ... }
exports['x3d_mailbox']:GetUnread(src) --> number (รวมทุกกล่อง)
exports['x3d_mailbox']:GetUnread(src, 'safe') --> number (เฉพาะตู้นิรภัย — ใส่ 'mail'/'safe' ได้)
GetMails กรองจดหมายที่หมดอายุออกให้อัตโนมัติ และแปะ label ภาษาไทยจาก Config.ItemLabels/Config.AccountLabels ลงบนสำเนาที่ส่งออกไป (ไม่แตะของจริงใน storage) GetUnread นับเฉพาะฉบับที่ยังไม่อ่านและไม่หมดอายุ คืนกลับเป็นตัวเลขเท่านั้น ไม่มี label
SendMail รับรูปแบบที่กล่องจดหมายเจ้าอื่นใช้ด้วย — subject, cash/bank, items = {{ name, count }}, expiresInDays — ย้ายจากกล่องจดหมายเก่าแค่เปลี่ยนชื่อ export
Events — server-internal (ไม่ใช่ net event)
AddEventHandler('x3d_mailbox:onSend', function(identifier, mail) end)
AddEventHandler('x3d_mailbox:onClaim', function(src, mail, ok) end)
x3d_mailbox:onSend ยิงทันทีที่จดหมายถูกสร้าง (ทั้งจาก export และคำสั่งแอดมิน)
x3d_mailbox:onClaim ยิงหลังพยายามจ่ายของเสมอ — mail.claimed เป็น true เสมอไม่ว่าจะสำเร็จหรือไม่ (ธงถูกปักไว้ก่อนจ่ายของโดยตั้งใจ กันรับซ้ำ) ต้องเช็กผลจริงที่ arg ที่ 3 — ok เป็น true เฉพาะตอนของแนบทุกชิ้นจ่ายสำเร็จไม่ throw:
AddEventHandler('x3d_mailbox:onClaim', function(src, mail, ok)
if not ok then print(('mail %s claimed by %s but a reward failed'):format(mail.id, src)) end
end)
คำสั่งแอดมิน
/mailsend <server id หรือ identifier> <หัวข้อ> <เนื้อหา…> และ /mailsendall <หัวข้อ> <เนื้อหา…> — เรียกจาก in-game console ได้เสมอ เรียกจากผู้เล่นในเกมต้องมี ace Config.AdminAce (ค่าเริ่มต้น x3d_mailbox.admin)
ความปลอดภัยที่ทดสอบไว้
- รับซ้ำไม่ได้ — ธง
claimedถูกเขียนลง storage ก่อนจ่ายของทุกครั้ง ไม่มี yield คั่นระหว่างเช็กกับปักธง บวกล็อกInFlight[identifier]กันสอง event ที่มาพร้อมกัน - ของพังไม่ล็อกผู้เล่นค้าง — การจ่ายของห่อด้วย
pcallจดหมายยังถูกทำเครื่องหมายว่ารับแล้วแม้จ่ายพัง (item key พิมพ์ผิดจาก/mailsendก็ไม่ทำให้ผู้เล่นกดรับซ้ำได้ไม่รู้จบ) — ได้ของบางส่วน (partial delivery) ผู้เล่นเห็นข้อความแจ้งแยกต่างหาก ไม่ถูกเงียบเป็น success และของที่จ่ายไปแล้วไม่ถูกย้อนคืน (กันจ่ายซ้ำ) - จำนวนถูกปัดเป็นจำนวนเต็มก่อนเสมอ — ของแนบที่เป็นเศษส่วนหรือติดลบเข้าไม่ถึงกระเป๋าผู้เล่น
- คีย์ผู้เล่นเป็น identifier ถาวรเสมอ ไม่ใช่ server id
- เพดาน
Config.MaxMailsไม่ลบจดหมายที่ยังไม่ได้รับของ — ล้นแล้วทิ้งฉบับที่รับของไปแล้วหรือหมดอายุแล้วก่อน (เก่าสุดก่อน) ฉบับที่แค่อ่านแล้วแต่ยังไม่ได้รับของและยังไม่หมดอายุ ของแนบยังอยู่ ไม่ถูกลบ - รับทั้งหมด = 1 action ไม่ใช่ N — วนจ่ายด้วยตรรกะเดียวกับกดรับทีละฉบับ เฉพาะกล่องที่เปิดแท็บอยู่ (จดหมาย/ตู้นิรภัยไม่ปนกัน) ฉบับที่กระเป๋าไม่พอถูกข้าม ยังอยู่ให้กดรับใหม่ทีหลัง ไม่มีฉบับไหนถูกจ่ายซ้ำ
- ลบได้เฉพาะจดหมายที่ไม่มีของค้างรับ — ต้องรับของก่อน (หรือไม่มีของแนบตั้งแต่แรก) — ข้อยกเว้นคือจดหมายที่หมดอายุแล้ว ลบได้เสมอเพราะรับของไม่ได้อีกต่อไปแล้ว (ของตกเป็นโมฆะ) — mail id ที่ client ส่งมาถูกมองหาเฉพาะในลิสต์ของผู้เล่นคนนั้นเองเท่านั้น
/redeemและ event รับโค้ดใช้ throttle + ล็อกเดียวกัน — สแปมคำสั่งรัว ๆ ไม่ทำให้ได้ของหลายรอบ ฟังก์ชันรับโค้ดเองก็ single-flight ต่อคน กันสอง call ชนกันแม้สลับไปใช้ storage แบบ await- คีย์
Config.Codesถูกปรับให้ตรงรูปแบบอัตโนมัติตอนเริ่ม — พิมพ์คีย์ตัวเล็กหรือผสมมาก็ไม่ตกหล่นเงียบ ๆ (มีคำเตือนใน console ถ้าต้องปรับ) - จำกัด 1 ครั้ง/วินาที/คนต่อ action (
open/read/claim/claimAll/delete/redeemแยก bucket)
pip install lupa
python tools/lua-test/x3d_mailbox_check.py
ตั้งค่า (config.lua)
| ค่า | ทำอะไร |
|---|---|
Config.Command / Config.Key |
คำสั่งเปิด (/mailbox) และปุ่มลัด (F7) |
Config.MaxMails |
เก็บต่อคนได้กี่ฉบับ |
Config.AdminAce |
ace ที่ใช้สั่ง /mailsend /mailsendall |
Config.Storage.File / .FlushSeconds |
ที่เก็บไฟล์ JSON และความถี่ flush |
Config.ItemLabels / Config.AccountLabels |
ชื่อไทยที่โชว์ในกล่องจดหมาย |
Config.ItemImage |
path รูปไอเทมของ inventory ที่เซิร์ฟใช้ ({item} = ชื่อไอเทม), ว่าง = ไม่โชว์รูป |
Config.ExpiryWarnHours |
จดหมายเหลือเวลาน้อยกว่านี้ (ชั่วโมง) โชว์นับถอยหลังสีแดงในพาเนล (ค่าเริ่มต้น 24) |
Config.CanCarryExport |
export ของ inventory ที่เซิร์ฟใช้ สำหรับเช็กว่ากระเป๋ารับไหว — ว่าง = ใช้ ESX canCarryItem · ขึ้น "กระเป๋าเต็ม" ทั้งที่ยังว่าง ให้ตั้งค่านี้ |
Config.DefaultExpireDays— จดหมายที่ส่งโดยไม่ระบุวันหมดอายุ จะหมดอายุเมื่อครบกี่วันที่ตั้งไว้ (ตู้นิรภัยไม่หมดอายุ)Config.CheckCarry—false= รับของได้แม้กระเป๋าเต็มConfig.NotifyUnreadOnJoin— แจ้งจำนวนจดหมายที่ยังไม่อ่านตอนผู้เล่นโหลดเข้าเกม- ชื่อไอเทมที่ไม่มีใน
Config.ItemLabelsจะใช้ชื่อจากเฟรมเวิร์กแทน
โค้ดรับของ (redeem code)
ผู้เล่นพิมพ์ /redeem <โค้ด> หรือกรอกโค้ดในช่องบนพาเนลกล่องจดหมายเลยก็ได้ แล้วของจะถูก ส่งเป็นจดหมาย เข้ากล่อง — กระเป๋าเต็มก็ไม่มีวันหาย เพราะไปรออยู่ในจดหมาย
โค้ดถาวรใส่ใน config.lua:
Config.Codes = {
WELCOME2026 = {
maxUses = 0, -- 0 = ไม่จำกัดจำนวนครั้งทั้งเซิร์ฟ
oncePerPlayer = true, -- คนละครั้งเดียว (ค่าเริ่มต้น)
title = 'ยินดีต้อนรับ',
items = { { item = 'bread', count = 2 } },
money = { { account = 'bank', amount = 1000 } },
},
EVENTONLY = {
maxUses = 100, -- 100 คนแรกเท่านั้น
expires = 1790000000, -- เวลา unix ที่โค้ดหมดอายุ
box = 'safe', -- ส่งเข้าตู้นิรภัยแทนกล่องจดหมาย (ไม่บังคับ, ค่าเริ่มต้น 'mail')
money = { { account = 'money', amount = 5000 } },
},
}
สร้างโค้ดตอนรัน (หายเมื่อรีสตาร์ท) จาก resource อื่น:
exports['x3d_mailbox']:AddCode('EVENT2026', { maxUses = 50, money = { { account = 'bank', amount = 2000 } } })
exports['x3d_mailbox']:RemoveCode('EVENT2026')
exports['x3d_mailbox']:GetCodeUsage('EVENT2026') --> { uses = 12, by = { ['license:xxx'] = true } }
exports['x3d_mailbox']:Redeem(src, 'EVENT2026') --> ok, reason
AddEventHandler('x3d_mailbox:onRedeem', function(src, code, def) end)
ยอดใช้โค้ดเก็บอยู่ในที่เดียวกับจดหมาย (คีย์ระบบ __redeem) — อยู่ครบหลังรีสตาร์ท และคำสั่งกระจายจดหมายทั้งเซิร์ฟไม่แตะคีย์นี้
ช่วยเหลือ
Discord: discord.gg/x3d-developer · เว็บ: x3d-developer.com