[{"data":1,"prerenderedAt":95},["ShallowReactive",2],{"site":3,"config-slug-site":15,"config-product-th-x3d-mailbox":21,"config-public-x3d-mailbox":90},{"site":4,"contact":10,"seo":13,"payment":14},{"name":5,"host":6,"default_locale":7,"locales":8},"X3D Developer","fivem.x3d-developer.com","th",[7,9],"en",{"discord_invite":11,"email":12,"line":12},"https:\u002F\u002Fdiscord.gg\u002FNA5kPVynFv","",{"google_site_verification":12,"bing_site_verification":12},{"bank_name":12,"account_name":12,"account_no":12,"promptpay_id":12},{"site":16,"contact":18,"seo":19,"payment":20},{"name":5,"host":6,"default_locale":7,"locales":17},[7,9],{"discord_invite":11,"email":12,"line":12},{"google_site_verification":12,"bing_site_verification":12},{"bank_name":12,"account_name":12,"account_no":12,"promptpay_id":12},{"id":22,"slug":23,"name":24,"summary":25,"price_thb":26,"license_mode":27,"cover_key":28,"cover_url":29,"category_slugs":30,"tags":32,"latest_version":51,"latest_released_at":52,"published_at":53,"has_docs":54,"config":55,"rent_price_satang":56,"description_md":57,"docs_md":58,"seo_title":59,"seo_description":60,"media":61,"versions":75},"83c7d7cb-bde4-45e3-a552-d7e377e4dc5d","x3d-mailbox","X3D Mailbox (โค้ดรับของ)","กล่องจดหมายออฟไลน์พร้อมของแนบ + โค้ดรับของ \u002Fredeem ฟรี ไม่ต้องลงฐานข้อมูล — ส่งไอเทมและเงินให้ผู้เล่นที่ออฟไลน์ แยกแท็บจดหมาย\u002Fตู้นิรภัย ต่อได้ทุกเฟรมเวิร์ก",0,"none","public\u002Fproducts\u002Fx3d-mailbox\u002Fcover\u002F743a9e72c1ae8d6d.jpg","\u002Fapi\u002Fv1\u002Fmedia\u002Fpublic\u002Fproducts\u002Fx3d-mailbox\u002Fcover\u002F743a9e72c1ae8d6d.jpg",[31],"gameplay",[33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],"esx","free","mailbox","redeem code","standalone","กล่องจดหมาย","ของแนบ","ฟรี","รีดีมโค้ด","ส่งของออฟไลน์","โค้ดรับของ","กรอกโค้ดรับของ","จดหมายออฟไลน์","ส่งของให้คนออฟไลน์","โค้ดแจกของ","แจกของกิจกรรม","ของชดเชย","ไม่ต้องใช้ฐานข้อมูล","1.7.2","2026-10-12T17:22:30.892527Z","2026-09-19T19:05:39.140076Z",true,"public",null,"# กล่องจดหมายออฟไลน์ พร้อมของแนบ — ฟรี ไม่ต้องลงฐานข้อมูล\n\nผู้เล่นออกจากเกมไปแล้ว แต่ยังอยากได้ของรางวัลกิจกรรม ของชดเชย หรือข้อความจากแอดมิน — `x3d_mailbox` ให้ส่งจดหมายพร้อมของแนบรอไว้ได้ ผู้เล่นมาเปิดรับเองตอนไหนก็ได้ ผ่านแผง Nuxt 4 UI พร้อมใช้ ต่อกับเฟรมเวิร์กไหนก็ได้ผ่านไฟล์ adapter\n\n---\n\n## ฟีเจอร์\n\n- [+] ตั้งค่าง่าย ปรับได้หมดใน config หรือตั้งผ่านเว็บได้เลย\n- [+] ส่งจดหมายพร้อมของแนบให้คนออฟไลน์ หรือทุกคนในเมืองได้\n- [+] แนบไอเทมหลายชิ้น และเงินเขียว เงินแดง ธนาคาร ในฉบับเดียว\n- [+] ตั้งวันหมดอายุจดหมายได้ มีเตือนสีแดง ตั้งอายุเริ่มต้นเองได้\n- [+] แยกแท็บจดหมาย \u002F ตู้นิรภัย นับ unread แยกกัน\n- [+] โค้ดรับของ \u002Fredeem จำกัดจำนวนครั้ง คนละครั้ง ตั้งวันหมดอายุได้\n- [+] กระเป๋าเต็มของก็ไม่หาย เพราะไปรออยู่ในจดหมาย\n- [+] กดรับทั้งหมดในคลิกเดียว กันรับซ้ำ กันสแปมรับของ\n- [+] แจ้งจำนวนจดหมายค้างตอนเข้าเกม\n- [+] เก็บเป็นไฟล์ JSON ไม่ต้องลงฐานข้อมูล หรือสลับเป็น MySQL ได้\n- [+] ย้ายมาจากกล่องจดหมายเจ้าอื่นง่าย SendMail รับรูปแบบเดิม\n- [+] เขียนเชื่อมกับสคริปต์อื่นได้ที่ addons\u002F\n\n---\n\n## ส่งของแนบได้ทั้งไอเทมและเงิน\n\n`SendMail`\u002F`SendMailAll` แนบไอเทมหลายชิ้น เงินหลายบัญชี (`money`, `bank`, `black_money`, หรือบัญชีอื่นของเฟรมเวิร์กคุณ) ในจดหมายฉบับเดียวได้ ตั้งวันหมดอายุได้ ตั้งชื่อผู้ส่งเองได้\n\nย้ายมาจากกล่องจดหมายตัวอื่น? `SendMail` รับรูปแบบที่กล่องจดหมายเจ้าอื่นใช้ด้วย (`subject`, `cash`\u002F`bank`, `items = {{ name, count }}`, `expiresInDays`) แค่เปลี่ยนชื่อ export ก็ย้ายได้เลย\n\n## ต่อกับเฟรมเวิร์กไหนก็ได้\n\n4 ไฟล์ใน `addons\u002F` — framework, notify, reward, storage — ค่าเริ่มต้นทำงานกับ ESX ได้ทันที ไม่มี ESX ก็ยังทำงานแบบ standalone ได้ (ace เท่านั้น) มีตัวอย่าง QBCore \u002F ox_lib คอมเมนต์ไว้ให้คัดลอกทับ\n\n## ไม่ต้องลงฐานข้อมูล\n\nเก็บข้อมูลเป็นไฟล์ JSON ในตัว resource เอง โหลดครั้งเดียว flush ทุก 30 วินาทีและตอนผู้เล่นออก\u002Fresource หยุดแบบปกติ — เซิร์ฟเวอร์ล่มกลางคันตรง ๆ ไม่ได้เสียของแนบ ความเสี่ยงจริงคือตรงข้าม: จดหมายที่ถูกรับในช่วง flush ล่าสุดที่ยังไม่ทัน (≤30 วินาที) ของแนบจ่ายผ่านเฟรมเวิร์กไปแล้ว แต่ธง `claimed` บนดิสก์ยังตามไม่ทัน รีสตาร์ทแล้วจดหมายฉบับเดิมอาจถูกรับซ้ำจ่ายซ้ำได้ อยากได้ MySQL สำหรับเซิร์ฟเวอร์ใหญ่? ตั้ง `Config.Storage.Driver = 'oxmysql'` ก็เก็บลงตาราง `x3d_mailbox` ให้เอง สร้างตารางอัตโนมัติ ไม่ต้อง include `@oxmysql`\n\n## รับซ้ำไม่ได้ ของพังไม่ล็อกผู้เล่นค้าง\n\nธงรับของถูกเขียนลง storage ก่อนจ่ายของทุกครั้ง event รับซ้ำที่มาพร้อมกันไม่มีทางจ่ายสองรอบ ถ้าไอเทมพิมพ์ผิดจนจ่ายของพัง จดหมายก็ยังถูกทำเครื่องหมายว่ารับแล้ว ไม่ค้างให้กดซ้ำได้ไม่รู้จบ จำนวนของแนบถูกปัดเป็นจำนวนเต็มก่อนเสมอ เพดานจำนวนจดหมายไม่มีวันลบฉบับที่ยังไม่ได้รับของทิ้ง\n\n## โค้ดรับของ (Redeem Code) ในตัว\n\nแจกโค้ดต้อนรับ โค้ดกิจกรรม หรือโค้ดชดเชยได้เลยโดยไม่ต้องลงสคริปอื่น ผู้เล่นพิมพ์ `\u002Fredeem \u003Cโค้ด>` หรือกรอกโค้ดในช่องบนพาเนลกล่องจดหมาย ของรางวัลทั้งไอเทมและเงินจะ**ส่งเป็นจดหมาย**เข้ากล่อง กระเป๋าเต็มก็ไม่หาย และเลือกให้ส่งเข้าแท็บตู้นิรภัยแทนได้\n\n- ตั้งโค้ดถาวรใน `Config.Codes`: จำกัดจำนวนครั้งทั้งเซิร์ฟ (`maxUses`) หนึ่งครั้งต่อคน (`oncePerPlayer`) และวันหมดอายุ (`expires`)\n- สร้างโค้ดชั่วคราวจากสคริปอื่นผ่าน export (`AddCode`, `RemoveCode`, `GetCodeUsage`) เช่นโค้ดไลฟ์สตรีมหรือโค้ดอีเวนต์\n- ยอดใช้โค้ดอยู่ครบหลังรีสตาร์ท และ `\u002Fredeem` ใช้ throttle กับล็อกเดียวกับการกดรับของ สแปมรัว ๆ ก็ได้ของครั้งเดียว\n\n## ต้องมี\n\nไม่มี · ไม่ต้องใช้ฐานข้อมูล ไม่ต้องใช้เฟรมเวิร์ก ไม่มีไลเซนส์ · วางแล้ว `ensure` ได้เลย\n\n**ฟรี** · แก้โค้ดได้ตามใจ · ขอแค่อย่านำไปขายต่อและอย่าอัปโหลดใหม่ในชื่อตัวเอง\n","## การติดตั้ง\n\n1. แตกไฟล์แล้ว **เปลี่ยนชื่อโฟลเดอร์เป็น `x3d_mailbox`** (สคริปต์อื่นเรียก export ด้วยชื่อนี้)\n2. วางใน `resources\u002F`\n3. เพิ่มใน `server.cfg`:\n\n```\nensure x3d_mailbox\n```\n\nเสร็จแล้ว — เก็บข้อมูลเป็นไฟล์ `data\u002Fmailbox.json` ในตัว resource เอง ไม่ต้องรัน SQL ใด ๆ\n\nผู้เล่นเปิดกล่องจดหมายด้วย `\u002Fmailbox` หรือปุ่ม **F7** (`Config.Key`, ว่าง = ไม่ผูกปุ่ม)\n\nในพาเนล: สองแท็บ **จดหมาย** (หมดอายุได้) กับ **ตู้นิรภัย** (ช่องที่สอง แยกนับ unread\u002Fรับทั้งหมดกันคนละกล่อง — ใช้ส่งของจากโค้ดรับของได้), กดรับทีละฉบับหรือ **รับทั้งหมด** ในคลิกเดียว (เฉพาะแท็บที่เปิดอยู่), **ลบ** จดหมายที่รับของแล้ว, นับถอยหลังเวลาหมดอายุ, แลกโค้ดผ่านช่องกรอกในพาเนล และเด้งแจ้งเตือนทันทีถ้าผู้เล่นออนไลน์อยู่ตอนได้จดหมายใหม่\n\nไม่ต้องใช้ฐานข้อมูล ไม่ต้องใช้เฟรมเวิร์ก ไม่มีไลเซนส์ · ทำงานกับ ESX ได้ทันที และทำงานแบบ standalone ได้ถ้าไม่มีเฟรมเวิร์ก\n\n## ต่อกับเฟรมเวิร์ก — 4 adapter ใน `addons\u002F`\n\n| โฟลเดอร์ | หน้าที่ |\n|---|---|\n| `addons\u002Fframework\u002F` | `Framework.IsBusy()` (client) — เปิดกล่องจดหมายตอนนี้ไม่ได้ไหม (ตาย\u002Fpause menu); `Framework.Identifier(src)` \u002F `Framework.IsAdmin(src)` (server) — คีย์ถาวรของผู้เล่นและสิทธิ์แอดมิน |\n| `addons\u002Fnotify\u002F` | `Notify(message, kind)` (client) — การแจ้งเตือน |\n| `addons\u002Freward\u002F` | `Reward.CanCarry` \u002F `Reward.GiveItem` \u002F `Reward.GiveMoney` (server) — ระบบไอเทม\u002Fเงิน |\n| `addons\u002Fstorage\u002F` | `Storage.Get\u002FSet\u002FAll\u002FFlush` (server) — ที่เก็บข้อมูล (ดูหัวข้อถัดไป) |\n\nทุกไฟล์มีค่าเริ่มต้นที่ใช้กับ ESX ได้ทันที (เช็ก `GetResourceState('es_extended')` เอง) และมีตัวอย่าง QBCore \u002F ox_lib คอมเมนต์ไว้ท้ายไฟล์ให้คัดลอกทับ ไม่มีเฟรมเวิร์กเลยก็ยังเปิดกล่องจดหมายได้ แค่รับของแนบไม่ได้จนกว่าจะต่อ `Reward.GiveItem`\u002F`GiveMoney` เอง (มี `print` เตือนใน console ถ้ายังไม่ได้ต่อ)\n\n## เก็บข้อมูล — JSON ค่าเริ่มต้น หรือสลับเป็น MySQL\n\nค่าเริ่มต้น: ไฟล์ JSON เดี่ยว (`Config.Storage.File`, ค่าเริ่มต้น `data\u002Fmailbox.json`) โหลดครั้งเดียวตอน resource เริ่ม เขียนกลับทุก `Config.Storage.FlushSeconds` วินาที (ค่าเริ่มต้น 30) และตอนผู้เล่นออก\u002Fresource หยุด ไฟล์เสียอ่านไม่ได้ → ย้ายไปเก็บเป็น `data\u002Fmailbox.broken.json` แล้วเริ่มไฟล์ใหม่ ไม่ทับทิ้งของเดิม\n\nอยากใช้ MySQL แทน:\n\n1. ตั้ง `Config.Storage.Driver = 'oxmysql'` — ตาราง `x3d_mailbox` ถูกสร้างให้อัตโนมัติ ไม่ต้อง include `@oxmysql`\n2. รีสตาร์ท resource เป็นอันเสร็จ (จะแก้ 4 ฟังก์ชัน `Storage.*` ใน `addons\u002Fstorage\u002Fserver.lua` ก็ต่อเมื่อต้องการที่เก็บข้อมูลแบบเขียนเอง)\n\n## Exports\n\n```lua\n-- target = server id (number) หรือ identifier (string) ก็ได้\nexports['x3d_mailbox']:SendMail(target, {\n    title   = 'ของรางวัลกิจกรรม',\n    body    = 'ขอบคุณที่เข้าร่วม',\n    sender  = 'ทีมงาน',           -- ไม่ใส่ = ใช้ชื่อกล่องจดหมาย\n    box     = 'mail',             -- 'mail' (ค่าเริ่มต้น, หมดอายุได้) หรือ 'safe' (ตู้ที่สอง)\n    expires = os.time() + 86400,  -- unix timestamp, ไม่ใส่ = ไม่หมดอายุ\n    items   = { { item = 'bread', count = 5 } },\n    money   = { { account = 'money', amount = 500 } },\n})\n\nexports['x3d_mailbox']:SendMailAll({ title = 'ประกาศ', body = 'เซิร์ฟเวอร์จะปิดปรับปรุงคืนนี้' })\n\nexports['x3d_mailbox']:GetMails(src)         --> { {id, title, body, sender, at, expires, read, claimed, box, items, money}, ... }\nexports['x3d_mailbox']:GetUnread(src)        --> number (รวมทุกกล่อง)\nexports['x3d_mailbox']:GetUnread(src, 'safe') --> number (เฉพาะตู้นิรภัย — ใส่ 'mail'\u002F'safe' ได้)\n```\n\n`GetMails` กรองจดหมายที่หมดอายุออกให้อัตโนมัติ และแปะ `label` ภาษาไทยจาก `Config.ItemLabels`\u002F`Config.AccountLabels` ลงบนสำเนาที่ส่งออกไป (ไม่แตะของจริงใน storage) `GetUnread` นับเฉพาะฉบับที่ยังไม่อ่านและไม่หมดอายุ คืนกลับเป็นตัวเลขเท่านั้น ไม่มี label\n\n`SendMail` รับรูปแบบที่กล่องจดหมายเจ้าอื่นใช้ด้วย — `subject`, `cash`\u002F`bank`, `items = {{ name, count }}`, `expiresInDays` — ย้ายจากกล่องจดหมายเก่าแค่เปลี่ยนชื่อ export\n\n## Events — server-internal (ไม่ใช่ net event)\n\n```lua\nAddEventHandler('x3d_mailbox:onSend', function(identifier, mail) end)\nAddEventHandler('x3d_mailbox:onClaim', function(src, mail, ok) end)\n```\n\n`x3d_mailbox:onSend` ยิงทันทีที่จดหมายถูกสร้าง (ทั้งจาก export และคำสั่งแอดมิน)\n\n`x3d_mailbox:onClaim` ยิงหลังพยายามจ่ายของเสมอ — **`mail.claimed` เป็น `true` เสมอไม่ว่าจะสำเร็จหรือไม่** (ธงถูกปักไว้ก่อนจ่ายของโดยตั้งใจ กันรับซ้ำ) ต้องเช็กผลจริงที่ arg ที่ 3 — `ok` เป็น `true` เฉพาะตอนของแนบทุกชิ้นจ่ายสำเร็จไม่ throw:\n\n```lua\nAddEventHandler('x3d_mailbox:onClaim', function(src, mail, ok)\n    if not ok then print(('mail %s claimed by %s but a reward failed'):format(mail.id, src)) end\nend)\n```\n\n## คำสั่งแอดมิน\n\n`\u002Fmailsend \u003Cserver id หรือ identifier> \u003Cหัวข้อ> \u003Cเนื้อหา…>` และ `\u002Fmailsendall \u003Cหัวข้อ> \u003Cเนื้อหา…>` — เรียกจาก in-game console ได้เสมอ เรียกจากผู้เล่นในเกมต้องมี ace `Config.AdminAce` (ค่าเริ่มต้น `x3d_mailbox.admin`)\n\n## ความปลอดภัยที่ทดสอบไว้\n\n- **รับซ้ำไม่ได้** — ธง `claimed` ถูกเขียนลง storage ก่อนจ่ายของทุกครั้ง ไม่มี yield คั่นระหว่างเช็กกับปักธง บวกล็อก `InFlight[identifier]` กันสอง event ที่มาพร้อมกัน\n- **ของพังไม่ล็อกผู้เล่นค้าง** — การจ่ายของห่อด้วย `pcall` จดหมายยังถูกทำเครื่องหมายว่ารับแล้วแม้จ่ายพัง (item key พิมพ์ผิดจาก `\u002Fmailsend` ก็ไม่ทำให้ผู้เล่นกดรับซ้ำได้ไม่รู้จบ) — ได้ของบางส่วน (partial delivery) ผู้เล่นเห็นข้อความแจ้งแยกต่างหาก ไม่ถูกเงียบเป็น success และของที่จ่ายไปแล้วไม่ถูกย้อนคืน (กันจ่ายซ้ำ)\n- **จำนวนถูกปัดเป็นจำนวนเต็มก่อนเสมอ** — ของแนบที่เป็นเศษส่วนหรือติดลบเข้าไม่ถึงกระเป๋าผู้เล่น\n- **คีย์ผู้เล่นเป็น identifier ถาวรเสมอ** ไม่ใช่ server id\n- **เพดาน `Config.MaxMails` ไม่ลบจดหมายที่ยังไม่ได้รับของ** — ล้นแล้วทิ้งฉบับที่รับของไปแล้วหรือหมดอายุแล้วก่อน (เก่าสุดก่อน) ฉบับที่แค่อ่านแล้วแต่ยังไม่ได้รับของและยังไม่หมดอายุ ของแนบยังอยู่ ไม่ถูกลบ\n- **รับทั้งหมด = 1 action ไม่ใช่ N** — วนจ่ายด้วยตรรกะเดียวกับกดรับทีละฉบับ เฉพาะกล่องที่เปิดแท็บอยู่ (จดหมาย\u002Fตู้นิรภัยไม่ปนกัน) ฉบับที่กระเป๋าไม่พอถูกข้าม ยังอยู่ให้กดรับใหม่ทีหลัง ไม่มีฉบับไหนถูกจ่ายซ้ำ\n- **ลบได้เฉพาะจดหมายที่ไม่มีของค้างรับ** — ต้องรับของก่อน (หรือไม่มีของแนบตั้งแต่แรก) — ข้อยกเว้นคือจดหมายที่หมดอายุแล้ว ลบได้เสมอเพราะรับของไม่ได้อีกต่อไปแล้ว (ของตกเป็นโมฆะ) — mail id ที่ client ส่งมาถูกมองหาเฉพาะในลิสต์ของผู้เล่นคนนั้นเองเท่านั้น\n- **`\u002Fredeem` และ event รับโค้ดใช้ throttle + ล็อกเดียวกัน** — สแปมคำสั่งรัว ๆ ไม่ทำให้ได้ของหลายรอบ ฟังก์ชันรับโค้ดเองก็ single-flight ต่อคน กันสอง call ชนกันแม้สลับไปใช้ storage แบบ await\n- **คีย์ `Config.Codes` ถูกปรับให้ตรงรูปแบบอัตโนมัติตอนเริ่ม** — พิมพ์คีย์ตัวเล็กหรือผสมมาก็ไม่ตกหล่นเงียบ ๆ (มีคำเตือนใน console ถ้าต้องปรับ)\n- จำกัด 1 ครั้ง\u002Fวินาที\u002Fคนต่อ action (`open`\u002F`read`\u002F`claim`\u002F`claimAll`\u002F`delete`\u002F`redeem` แยก bucket)\n\n```bash\npip install lupa\npython tools\u002Flua-test\u002Fx3d_mailbox_check.py\n```\n\n## ตั้งค่า (`config.lua`)\n\n| ค่า | ทำอะไร |\n|---|---|\n| `Config.Command` \u002F `Config.Key` | คำสั่งเปิด (`\u002Fmailbox`) และปุ่มลัด (`F7`) |\n| `Config.MaxMails` | เก็บต่อคนได้กี่ฉบับ |\n| `Config.AdminAce` | ace ที่ใช้สั่ง `\u002Fmailsend` `\u002Fmailsendall` |\n| `Config.Storage.File` \u002F `.FlushSeconds` | ที่เก็บไฟล์ JSON และความถี่ flush |\n| `Config.ItemLabels` \u002F `Config.AccountLabels` | ชื่อไทยที่โชว์ในกล่องจดหมาย |\n| `Config.ItemImage` | path รูปไอเทมของ inventory ที่เซิร์ฟใช้ (`{item}` = ชื่อไอเทม), ว่าง = ไม่โชว์รูป |\n| `Config.ExpiryWarnHours` | จดหมายเหลือเวลาน้อยกว่านี้ (ชั่วโมง) โชว์นับถอยหลังสีแดงในพาเนล (ค่าเริ่มต้น 24) |\n| `Config.CanCarryExport` | export ของ inventory ที่เซิร์ฟใช้ สำหรับเช็กว่ากระเป๋ารับไหว — ว่าง = ใช้ ESX `canCarryItem` · **ขึ้น \"กระเป๋าเต็ม\" ทั้งที่ยังว่าง ให้ตั้งค่านี้** |\n\n- `Config.DefaultExpireDays` — จดหมายที่ส่งโดยไม่ระบุวันหมดอายุ จะหมดอายุเมื่อครบกี่วันที่ตั้งไว้ (ตู้นิรภัยไม่หมดอายุ)\n- `Config.CheckCarry` — `false` = รับของได้แม้กระเป๋าเต็ม\n- `Config.NotifyUnreadOnJoin` — แจ้งจำนวนจดหมายที่ยังไม่อ่านตอนผู้เล่นโหลดเข้าเกม\n- ชื่อไอเทมที่ไม่มีใน `Config.ItemLabels` จะใช้ชื่อจากเฟรมเวิร์กแทน\n\n## โค้ดรับของ (redeem code)\n\nผู้เล่นพิมพ์ `\u002Fredeem \u003Cโค้ด>` หรือกรอกโค้ดในช่องบนพาเนลกล่องจดหมายเลยก็ได้ แล้วของจะถูก **ส่งเป็นจดหมาย** เข้ากล่อง — กระเป๋าเต็มก็ไม่มีวันหาย เพราะไปรออยู่ในจดหมาย\n\nโค้ดถาวรใส่ใน `config.lua`:\n\n```lua\nConfig.Codes = {\n    WELCOME2026 = {\n        maxUses       = 0,        -- 0 = ไม่จำกัดจำนวนครั้งทั้งเซิร์ฟ\n        oncePerPlayer = true,     -- คนละครั้งเดียว (ค่าเริ่มต้น)\n        title         = 'ยินดีต้อนรับ',\n        items         = { { item = 'bread', count = 2 } },\n        money         = { { account = 'bank', amount = 1000 } },\n    },\n    EVENTONLY = {\n        maxUses = 100,            -- 100 คนแรกเท่านั้น\n        expires = 1790000000,     -- เวลา unix ที่โค้ดหมดอายุ\n        box     = 'safe',         -- ส่งเข้าตู้นิรภัยแทนกล่องจดหมาย (ไม่บังคับ, ค่าเริ่มต้น 'mail')\n        money   = { { account = 'money', amount = 5000 } },\n    },\n}\n```\n\nสร้างโค้ดตอนรัน (หายเมื่อรีสตาร์ท) จาก resource อื่น:\n\n```lua\nexports['x3d_mailbox']:AddCode('EVENT2026', { maxUses = 50, money = { { account = 'bank', amount = 2000 } } })\nexports['x3d_mailbox']:RemoveCode('EVENT2026')\nexports['x3d_mailbox']:GetCodeUsage('EVENT2026')   --> { uses = 12, by = { ['license:xxx'] = true } }\nexports['x3d_mailbox']:Redeem(src, 'EVENT2026')    --> ok, reason\n\nAddEventHandler('x3d_mailbox:onRedeem', function(src, code, def) end)\n```\n\nยอดใช้โค้ดเก็บอยู่ในที่เดียวกับจดหมาย (คีย์ระบบ `__redeem`) — อยู่ครบหลังรีสตาร์ท และคำสั่งกระจายจดหมายทั้งเซิร์ฟไม่แตะคีย์นี้\n\n## ช่วยเหลือ\n\nDiscord: discord.gg\u002Fx3d-developer · เว็บ: x3d-developer.com\n","สคริป FiveM กล่องจดหมาย + โค้ดรับของ (Redeem Code) ฟรี","สคริป FiveM ฟรี กล่องจดหมายออฟไลน์ + โค้ดรับของ \u002Fredeem ของรางวัลส่งเป็นจดหมาย กระเป๋าเต็มก็ไม่หาย จำกัดสิทธิ์ต่อคน ไม่ต้องลงฐานข้อมูล ใช้ได้ทุกเฟรมเวิร์ก",[62,65,67,69,71,73],{"kind":63,"url":64},"image","\u002Fapi\u002Fv1\u002Fmedia\u002Fpublic\u002Fproducts\u002Fx3d-mailbox\u002Fmedia\u002F5f0e2bffeb5847dd.png",{"kind":63,"url":66},"\u002Fapi\u002Fv1\u002Fmedia\u002Fpublic\u002Fproducts\u002Fx3d-mailbox\u002Fmedia\u002Fecae52ada3262923.png",{"kind":63,"url":68},"\u002Fapi\u002Fv1\u002Fmedia\u002Fpublic\u002Fproducts\u002Fx3d-mailbox\u002Fmedia\u002Fdfbe3182ca82566c.png",{"kind":63,"url":70},"\u002Fapi\u002Fv1\u002Fmedia\u002Fpublic\u002Fproducts\u002Fx3d-mailbox\u002Fmedia\u002F3b10675e2258a5ab.png",{"kind":63,"url":72},"\u002Fapi\u002Fv1\u002Fmedia\u002Fpublic\u002Fproducts\u002Fx3d-mailbox\u002Fmedia\u002Fa1d612aa5b9a172d.png",{"kind":63,"url":74},"\u002Fapi\u002Fv1\u002Fmedia\u002Fpublic\u002Fproducts\u002Fx3d-mailbox\u002Fmedia\u002F2f18c7395c42734f.png",[76,78,82,86],{"version":51,"released_at":52,"changelog_md":77},"### Changed\n\n- Store images now carry English captions for the English store page",{"version":79,"released_at":80,"changelog_md":81},"1.7.0","2026-10-12T03:25:54.186504Z","### Changed\n\n- UI font is now Kanit (one family for Thai and English), bundled inside the script; the Thai font now really loads in game (FiveM's browser dropped the old font declaration and fell back to a system font)\n### Added\n- `SendMail` also accepts the shape other mailboxes use (`subject`, `cash`\u002F`bank`, `items = {{ name, count }}`, `expiresInDays`) — moving off an old mailbox is a rename of the export\n- `Config.Storage.Driver = 'oxmysql'` keeps mail in table `x3d_mailbox` (created automatically; no `@oxmysql` include needed)\n- `Config.DefaultExpireDays` (mail without an expiry expires after N days; the safe never expires), `Config.CheckCarry` (false = claim even with a full bag), `Config.NotifyUnreadOnJoin` (unread count when a player loads in)\n### Fixed\n- Item names fall back to the framework's item label when `Config.ItemLabels` has no entry\n- Claims were refused on servers whose inventory replaces ESX's (ESX's stale weight vetoed every item); the carry check now asks `Config.CanCarryExport` first everywhere, including inside item delivery",{"version":83,"released_at":84,"changelog_md":85},"1.4.0","2026-09-20T10:38:20.840557Z","### Added\r\n- Safe box: a second mail channel (`box = 'safe'` on any mail, `SendMail`\u002Fredeem codes accept it, defaults to `'mail'`) with its own unread count, its own Receive All (never touches the other box), and its own tab + badge in the panel\r\n- `GetUnread(src, box)` accepts an optional box to scope the count; omitted, it still counts every box like before\r\n\r\n### Fixed\r\n- **Critical:** `\u002Fredeem` bypassed the rate limit entirely (only the net event was throttled) — same-tick command spam could drain a whole `maxUses` pool from one player; the command now shares `doRedeem`'s throttle with the net event, and `Mail.Redeem` itself is single-flight per identifier so a concurrent call (including a direct `exports.x3d_mailbox:Redeem` from another resource) can't race a still-in-flight redeem even once storage starts yielding\r\n- **Critical:** a `Config.Codes` key not already matching the case `\u002Fredeem` compares against (uppercase unless `caseSensitive = true`) was silently unredeemable — keys are now normalised once at boot, with a console warning naming any key that had to change\r\n- Delete now shares the same `InFlight` lock as claim\u002FclaimAll, closing a window where a delete and a claim on the same list could interleave under a storage adapter that doesn't cache in memory (e.g. the documented oxmysql swap)\r\n- A partial delivery (some attachments paid, some failed) now tells the player with its own message (`partial_delivery`) instead of staying silent — the mail is still never reverted, so what already landed is never paid twice\r\n- An expired mail with unclaimed attachments could never be deleted or trimmed by `Config.MaxMails` — its reward is already forfeit once it expires, so it's now deletable and gets reaped by the cap like a claimed mail\r\n- `x3d_mailbox:read` had no throttle bucket — it's rate-limited like every other action now\r\n- `Mail.Add` stored the caller's `items`\u002F`money` tables by reference — `SendMailAll` and a `Config.Codes` entry reused across redemptions could alias into storage; both are now copied on the way in",{"version":87,"released_at":88,"changelog_md":89},"1.0.0","2026-09-19T19:05:37.383308Z","### Added\n- Offline mailbox: mails with title, body, sender, optional expiry, item and money attachments\n- Exports `SendMail(target, mail)`, `SendMailAll(mail)`, `GetMails(src)`, `GetUnread(src)`\n- Server-internal events `x3d_mailbox:onSend(identifier, mail)` and `x3d_mailbox:onClaim(src, mail, ok)`\n- Nuxt 4 panel, `\u002Fmailbox` command and F7 keybind\n- Admin commands `\u002Fmailsend` and `\u002Fmailsendall`, gated by the `x3d_mailbox.admin` ace\n- Claim-once ordering: the claimed flag is written to storage before payout, with an `InFlight` per-identifier lock against simultaneous claims\n- Reward payout wrapped in `pcall` so a broken item\u002Faccount can't leave a player permanently unable to claim\n- Attachment counts floored to whole numbers before every capacity check and payout\n- JSON file storage by default, with a commented oxmysql swap and `install.sql`\n- Four `addons\u002F` adapter files (framework, notify, reward, storage) with ESX defaults and QBCore\u002Fox_lib examples\n- `Config.ItemLabels` \u002F `Config.AccountLabels` for Thai display names",{"slug":23,"version":51,"latest_version":51,"files":91,"options":56},[92],{"path":93,"content":94},"config.lua","Config = {}\r\n\r\n-- เฟรมเวิร์กที่ใช้: 'auto' (ESX ถ้ามี ไม่มีก็ standalone) | 'esx' | 'standalone'\r\nConfig.Framework = 'auto'\r\n\r\nConfig.Locale  = 'th'\r\nConfig.Command = 'mailbox'          -- \u002Fmailbox\r\nConfig.Key     = 'F7'               -- '' = ไม่ผูกปุ่ม\r\nConfig.MaxMails = 50                -- เก็บต่อคนได้กี่ฉบับ (ล้นแล้วทิ้งฉบับที่อ่าน\u002Fรับของแล้วก่อน)\r\nConfig.AdminAce = 'x3d_mailbox.admin'\r\n\r\n-- จดหมายที่เหลือเวลาน้อยกว่านี้ (ชั่วโมง) จะโชว์เตือนสีแดงในพาเนล\r\nConfig.ExpiryWarnHours = 24\r\n\r\n-- จดหมาย (ไม่ใช่ตู้นิรภัย) ที่ผู้ส่งไม่กำหนดวันหมดอายุ จะหมดอายุใน N วัน (0 = ไม่หมดอายุ)\r\n-- Mail (not the safe) sent without an expiry expires after N days (0 = never)\r\nConfig.DefaultExpireDays = 0\r\n\r\n-- true = เช็กว่ากระเป๋ารับไหวก่อนเปิดจดหมาย · false = รับได้เสมอ ให้ inventory จัดการของล้นเอง\r\n-- true = check the bag can carry it before opening a mail · false = always claim, the inventory handles overflow\r\nConfig.CheckCarry = true\r\n\r\n-- แจ้งจำนวนจดหมายที่ยังไม่อ่านตอนเข้าเกม\r\n-- Tell players how many unread mails they have when they load in\r\nConfig.NotifyUnreadOnJoin = true\r\n\r\nConfig.Storage = {\r\n    -- 'file' = ไฟล์ JSON ในโฟลเดอร์สคริปต์ (ไม่ต้องมีฐานข้อมูล) · 'oxmysql' = ตาราง x3d_mailbox (แนะนำเมื่อคนเยอะ)\r\n    -- 'file' = a JSON file in the script folder (no database) · 'oxmysql' = table x3d_mailbox (recommended for busy servers)\r\n    Driver       = 'file',\r\n    File         = 'data\u002Fmailbox.json',\r\n    FlushSeconds = 30,\r\n}\r\n\r\n-- ชื่อไอเทมที่จะโชว์ในกล่องจดหมาย (ไม่ใส่ก็ได้ — ไม่ใส่จะโชว์ชื่อ key ตรง ๆ)\r\nConfig.ItemLabels = {\r\n    -- bread = 'ขนมปัง',\r\n    -- water = 'น้ำดื่ม',\r\n}\r\n\r\nConfig.AccountLabels = {\r\n    money       = 'เงินสด',\r\n    bank        = 'ธนาคาร',\r\n    black_money = 'เงินดำ',\r\n}\r\n\r\n-- รูปไอเทมในกล่องจดหมาย — ใส่ path รูปของ inventory ที่เซิร์ฟใช้ ({item} = ชื่อไอเทม)\r\n-- ปล่อยเป็น '' = ไม่โชว์รูป โชว์แค่ชื่อไอเทม\r\nConfig.ItemImage = ''\r\n-- Config.ItemImage = 'nui:\u002F\u002Fx3d_inventory\u002Fweb\u002Fbuild\u002Fimg\u002Fitems\u002F{item}.png'\r\n-- Config.ItemImage = 'nui:\u002F\u002Fox_inventory\u002Fweb\u002Fimages\u002F{item}.png'\r\n-- Config.ItemImage = 'nui:\u002F\u002Fqb-inventory\u002Fhtml\u002Fimages\u002F{item}.png'\r\n-- Config.ItemImage = 'nui:\u002F\u002Fesx_inventoryhud\u002Fhtml\u002Fimg\u002Fitems\u002F{item}.png'\r\n-- ไม่มีในลิสต์นี้? เปิดโฟลเดอร์รูปไอเทมของ inventory resource ที่ใช้อยู่ แล้วคัดลอก path มาแทน\r\n\r\n-- เช็กว่ากระเป๋ารับของไหวก่อนเปิดจดหมาย — ใส่เฉพาะตอนเซิร์ฟใช้ inventory ของตัวเอง\r\n-- (x3d_inventory\u002Fox_inventory ฯลฯ) ไม่ใช่ ESX ตรง ๆ เพราะเลขน้ำหนักของ ESX เองจะเพี้ยน\u002F\r\n-- ไม่อัปเดตให้ ทำให้จดหมายที่มีของแนบถูกปฏิเสธว่า \"กระเป๋าเต็ม\" ทั้งที่จริงยังว่าง\r\n-- ปล่อยว่าง = เช็กผ่าน ESX canCarryItem ถ้ามี ESX ไม่มีก็ปล่อยผ่าน (allow)\r\nConfig.CanCarryExport = { resource = '', method = '' }\r\n-- Config.CanCarryExport = { resource = 'x3d_inventory', method = 'CanCarryItemSrc' }\r\n-- Config.CanCarryExport = { resource = 'ox_inventory', method = 'CanCarryItem' }\r\n-- ไม่มีในลิสต์นี้? เปิด export ของ inventory resource ที่ใช้อยู่ หาฟังก์ชันเช็กว่ารับของไหวไหม\r\n\r\n-- ═══════════════════════════════════════════════════════════════\r\n--  โค้ดรับของ (redeem code)\r\n--  ผู้เล่นพิมพ์ \u002Fredeem \u003Cโค้ด> แล้วของจะถูก \"ส่งเป็นจดหมาย\" เข้ากล่อง\r\n--  กระเป๋าเต็มก็ไม่มีวันหาย เพราะไปรออยู่ในจดหมาย\r\n-- ═══════════════════════════════════════════════════════════════\r\n\r\nConfig.Redeem = {\r\n    enabled       = true,\r\n    command       = 'redeem',\r\n    caseSensitive = false,      -- false = พิมพ์ตัวเล็กตัวใหญ่ก็ได้\r\n}\r\n\r\n-- โค้ดถาวร — แก้ที่นี่แล้ว restart resource\r\n-- ⚠ คีย์ (เช่น WELCOME2026) จะถูกปรับให้ตรงรูปแบบที่ \u002Fredeem เทียบตอน resource เริ่ม\r\n--   (ตัวพิมพ์ใหญ่ทั้งหมดเมื่อ Config.Redeem.caseSensitive = false) — พิมพ์เล็กพิมพ์ใหญ่ผสมกันได้\r\n--   จะมีคำเตือนใน console ถ้าคีย์ที่ตั้งไว้ไม่ตรงแล้วถูกปรับให้อัตโนมัติ\r\n--   maxUses       จำนวนครั้งที่ใช้ได้ทั้งเซิร์ฟ (0 = ไม่จำกัด)\r\n--   oncePerPlayer true = คนละครั้งเดียว (ค่าเริ่มต้น)\r\n--   expires       เวลา unix ที่โค้ดหมดอายุ (ไม่ใส่ = ไม่หมดอายุ)\r\n--   box           'mail' (ค่าเริ่มต้น, หมดอายุได้) หรือ 'safe' (ตู้ที่สอง แยกกันในพาเนล)\r\n--   title\u002Fbody    ข้อความบนจดหมายที่ผู้เล่นได้รับ (ไม่ใส่ก็มีข้อความมาตรฐานให้)\r\nConfig.Codes = {\r\n    -- WELCOME2026 = {\r\n    --     maxUses       = 0,\r\n    --     oncePerPlayer = true,\r\n    --     title         = 'ยินดีต้อนรับ',\r\n    --     body          = 'ขอบคุณที่เข้ามาเล่นกับเรา',\r\n    --     items         = { { item = 'bread', count = 2 } },\r\n    --     money         = { { account = 'bank', amount = 1000 } },\r\n    -- },\r\n    -- EVENTONLY = {\r\n    --     maxUses = 100,                    -- 100 คนแรกเท่านั้น\r\n    --     expires = 1790000000,             -- เวลา unix\r\n    --     money   = { { account = 'money', amount = 5000 } },\r\n    -- },\r\n}\r\n",1792316309347]