Developer Documentation

BuddyTH License
System

ระบบตรวจสอบและจัดการ License สำหรับ FiveM Scripts — ป้องกันการใช้งานที่ไม่ได้รับอนุญาต ผ่านการยืนยัน License Key, Server IP และ Resource Name แบบ Real-time

FiveM Compatible ESX / QBCore Real-time Verify IP Verification Simple Prepend
01
How it Works
ระบบทำงานโดยใช้ Wrapper + Load Gate Architecture
Server Wrapper
ตรวจสอบ License กับ API ตอน resource start แล้วแจ้งผลให้ Client ผ่าน NetEvent ทุก request มี HMAC Secret ยืนยัน
Client Integration
client.lua เชื่อมต่อกับ server ผ่าน NetEvent และรอ Config จาก Server ก่อนเริ่มทำงาน
IP Verification
ระบบดึง Public IP ของ Server อัตโนมัติผ่าน ipify.org และเปรียบเทียบกับ IP ที่ลงทะเบียนไว้ใน Database
Auto Retry
Client Wrapper จะ retry ขอสถานะจาก Server สูงสุด 15 ครั้ง (ทุก 1.5s) ก่อนถือว่าไม่ผ่านและ block script
Verification Flow
1
Resource Start
FiveM โหลด resource — Server Wrapper ถูก execute ก่อนโค้ดต้นฉบับ รอ 2 วินาทีก่อนยิง API
2
Get Public IP
ยิง api.ipify.org เพื่อรับ Public IP จริงของ Server (fallback: sv_endpoints)
3
POST to License API
ส่ง license_key, resource, server_ip, timestamp, secret ไปตรวจสอบ
4
Broadcast to Clients
Server แจ้งผลการตรวจสอบให้ Client ผ่าน bdyth:cl:<resource> NetEvent
5
Client Gate Decision
PASS → Client โหลด ESX และขอ Config จาก Server  |  FAIL → Server หยุด resource ทันที
6
Stop Resource
ถ้า License ไม่ผ่าน — Server จะ StopResource() ทันที (Hardcoded, ปิดไม่ได้)
02
Quick Setup
ขั้นตอนเพิ่ม License System เข้า FiveM Resource ของคุณ
หากใช้ระบบ Auto-Download จากหน้า Admin → Licenses ทุกขั้นตอนจะทำอัตโนมัติ ไม่จำเป็นต้องทำด้วยตนเอง

01 สร้าง config.license.lua

ไฟล์นี้เก็บ licenseKey ของ resource — ระบบจะ generate ให้อัตโนมัติเมื่อ Download

config.license.lua
ConfigLicense = ConfigLicense or {} -- License Key สำหรับ resource นี้ (ได้รับจาก BuddyTH Shop) ConfigLicense.licenseKey = "XXXX-XXXX-XXXX-XXXX" -- หยุด resource ทันทีเมื่อ License ไม่ผ่าน (แนะนำ: true) ConfigLicense.stopOnFail = true

02 แก้ไข fxmanifest.lua

เพิ่ม config.license.lua เข้า shared_scripts เพื่อให้ทั้ง server และ client อ่านได้

fxmanifest.lua
fx_version 'cerulean' game 'gta5' -- เพิ่ม shared_scripts ให้ทั้ง server และ client เข้าถึงได้ shared_scripts { 'config/config.license.lua', -- ← เพิ่มบรรทัดนี้ 'config.lua', } server_scripts { 'server.lua' } client_scripts { 'client.lua' }
ต้องใช้ shared_scripts เท่านั้น — ไม่ใช่ server_scripts เพียงอย่างเดียว เพราะ Client Wrapper ต้องอ่าน ConfigLicense.licenseKey ด้วย
03
Server Block
โค้ด Lua ที่ prepend บนสุดของ server.lua — ทำหน้าที่ตรวจสอบ License

Server Block วางไว้บนสุดของ server.lua — โค้ดต้นฉบับเดิมต่อจากนี้ได้เลย ไม่ต้องแก้ไขส่วนอื่นใด

Net Events ที่ถูกสร้าง
Event NameDirectionคำอธิบาย
bdyth:sv:<resource> Client → Server Client ขอสถานะ License จาก Server
bdyth:cl:<resource> Server → Client Server ส่งผลการตรวจสอบ (ok, msg, code) กลับ
server.lua — prepend ที่บนสุด
-- server.lua local API_URL = "https://shop.buddyth.com/api/license/verify" local SECRET = "YOUR_SECRET_HERE" local licenseOk = false local licenseMessage = "Checking..." local licenseData = {} local __LICENSE_READY = false ----------------------------------------------------------------- -- ดึง IP จริงของ Server อัตโนมัติ ----------------------------------------------------------------- local function getServerIp(callback) PerformHttpRequest("https://api.ipify.org?format=json", function(statusCode, body) if statusCode == 200 and body then local ok, data = pcall(json.decode, body) if ok and data and data.ip then return callback(data.ip) end end local ip = GetConvar("sv_endpoints", "unknown") callback(ip) end, "GET", "", {}) end ----------------------------------------------------------------- -- ยิง API ตรวจสอบ License ----------------------------------------------------------------- local function verifyLicense(callback) getServerIp(function(serverIp) local payload = { license_key = (ConfigLicense and ConfigLicense.licenseKey) or "", resource = GetCurrentResourceName(), server_ip = serverIp, timestamp = os.time(), secret = SECRET } print("[LICENSE] Verifying resource: " .. payload.resource .. " | IP: " .. serverIp) PerformHttpRequest( API_URL, function(statusCode, body, headers) if statusCode ~= 200 or not body then licenseOk = false licenseMessage = "API connection failed" licenseData = {} return callback(false) end local ok, data = pcall(json.decode, body) if not ok or type(data) ~= "table" then licenseOk = false licenseMessage = "Invalid API response" licenseData = {} return callback(false) end if data.status == true then licenseOk = true licenseMessage = data.message or "License valid" licenseData = data.data or {} callback(true) else licenseOk = false licenseMessage = data.message or "License invalid" licenseData = {} callback(false) end end, "POST", json.encode(payload), { ["Content-Type"] = "application/json" } ) end) end ----------------------------------------------------------------- -- พิมพ์ Banner ----------------------------------------------------------------- local function printSuccessBanner() print("^2") print(" ███████╗██╗ ██╗ ██████╗ ██████╗ ███████╗███████╗███████╗") print(" ██╔════╝██║ ██║██╔════╝██╔════╝ ██╔════╝██╔════╝██╔════╝") print(" ███████╗██║ ██║██║ ██║ █████╗ ███████╗███████╗") print(" ╚════██║██║ ██║██║ ██║ ██╔══╝ ╚════██║╚════██║") print(" ███████║╚██████╔╝╚██████╗╚██████╗ ███████╗███████║███████║") print(" ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝") print("^7") print("^2 [ LICENSE VERIFIED SUCCESSFULLY ]^7") print("^3 Resource : " .. GetCurrentResourceName()) print("^3 Message : " .. tostring(licenseMessage)) print("^2 Status : AUTHORIZED^7") print(" ") end local function printFailBanner() print("^1") print(" ███████╗ █████╗ ██╗██╗ ") print(" ██╔════╝██╔══██╗██║██║ ") print(" █████╗ ███████║██║██║ ") print(" ██╔══╝ ██╔══██║██║██║ ") print(" ██║ ██║ ██║██║███████╗") print(" ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝") print("^7") print("^1[LICENSE] FAILED : " .. tostring(licenseMessage) .. "^7") print("^1╔══════════════════════════════════════╗^7") print("^1║ shop.buddyth.com ║^7") print("^1║ LICENSE VERIFICATION ║^7") print("^1║ STATUS : FAIL ║^7") print("^1╚══════════════════════════════════════╝^7") end ----------------------------------------------------------------- -- ตรวจสอบตอน resource start ----------------------------------------------------------------- CreateThread(function() Wait(2000) verifyLicense(function(ok) __LICENSE_READY = true if ok then printSuccessBanner() else printFailBanner() if ConfigLicense and ConfigLicense.stopOnFail then StopResource(GetCurrentResourceName()) end end end) end)
04
Client Block
โค้ดส่วนที่จำเป็นสำหรับ client.lua — วางไว้บนสุดของไฟล์
client.lua — หลัง inject wrapper
----------------------------------------------------------------- -- รอ ESX โหลด ----------------------------------------------------------------- Citizen.CreateThread(function() while ESX == nil do TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) Citizen.Wait(0) end end) ----------------------------------------------------------------- -- ขอ Config จาก Server ----------------------------------------------------------------- Citizen.CreateThread(function() while not ESX do Citizen.Wait(100) end TriggerServerEvent('campfire:requestConfig') end) ----------------------------------------------------------------- -- รับ Config จาก Server ----------------------------------------------------------------- RegisterNetEvent('campfire:receiveConfig') AddEventHandler('campfire:receiveConfig', function(config) if config then Config = config if Config.campfire and Config.campfire.showMarker ~= nil then showMarkers = Config.campfire.showMarker end end end)
05
API Reference
License Verification Endpoint
POST https://shop.buddyth.com/api/license/verify

Request Body

FieldTypeตัวอย่างStatus
license_keystringABCD-EFGH-IJKL-MNOPrequired
resourcestringmy_scriptrequired
server_ipstring203.154.1.1required
timestampinteger1718000000required — Unix time
secretstringe7c57db6...required — 64-char hex

Response Format

License Valid
200 OK
{ "status": true, "message": "License valid", "code": "OK", "data": { "expires": "2026-12-31" } }
License Invalid
200 OK
{ "status": false, "message": "IP not registered", "code": "IP_MISMATCH", "data": {} }
06
Security Rules
พฤติกรรมที่กำหนดตายตัว — ไม่สามารถเปลี่ยนแปลงได้
Hardcoded Behaviour: ค่าต่อไปนี้ถูก hardcode ใน wrapper และไม่สามารถปิดหรือเปลี่ยนได้จาก config ใด ๆ
stopOnFail (config)
StopResource() จะถูกเรียกเมื่อ License ไม่ผ่าน หากตั้ง ConfigLicense.stopOnFail = true
Secret Verification
ทุก request ต้องส่ง SECRET 64-char hex ที่ตรงกับ Database จึงจะผ่านการตรวจสอบ
2s Startup Delay
ระบบรอ 2 วินาทีหลัง resource start ก่อนยิง API เพื่อให้ทุก dependency โหลดเสร็จก่อน
IP Auto-Detect
ดึง Public IP จาก api.ipify.org อัตโนมัติ — fallback เป็น sv_endpoints หาก API ล่ม
07
Error Codes
รหัสข้อผิดพลาดจาก API และวิธีแก้ไข
Codeความหมายวิธีแก้ไข
OK License ผ่าน
IP_MISMATCH IP ของ Server ไม่ตรงกับที่ลงทะเบียน อัปเดต Server IP ในหน้า Dashboard หรือติดต่อ Admin
KEY_NOT_FOUND ไม่พบ License Key ในระบบ ตรวจสอบ licenseKey ใน config.license.lua
EXPIRED License หมดอายุแล้ว ต่ออายุ License ผ่านหน้า BuddyTH Shop
INACTIVE License ถูก disable ติดต่อ Admin เพื่อเปิดใช้งาน
SECRET_MISMATCH Secret ไม่ตรงกับใน Database Download สคริปต์ใหม่จากระบบเพื่อรับ Secret ล่าสุด
NETWORK_ERROR เชื่อมต่อ API ไม่ได้ ตรวจสอบ sv_endpoints และ firewall ของ Server
TIMEOUT Client ไม่ได้รับสถานะจาก Server ตรวจสอบว่า _TAG ทั้ง server/client ตรงกัน
08
FAQ
คำถามที่พบบ่อย
ต้องเปลี่ยน Secret เมื่อไหร่?

Secret ถูก generate ครั้งเดียวตอนสร้าง License และจะฝังอยู่ใน server wrapper อัตโนมัติเมื่อ Download ไม่ต้องเปลี่ยนเว้นแต่สงสัยว่ามีการ leak — ในกรณีนั้นติดต่อ Admin เพื่อ Regenerate

ใช้กับ QBCore ได้ไหม?

ได้ — Server Wrapper ใช้ esx:playerLoaded สำหรับ push สถานะให้ผู้เล่นที่ join ทีหลัง สำหรับ QBCore ให้เปลี่ยนเป็น QBCore:Server:PlayerLoaded แทน

1 License ใช้ได้กี่ Server?

1 License ต่อ 1 Server IP — หากต้องการใช้หลาย Server ต้องซื้อ License แยกต่างหาก สามารถอัปเดต IP ได้ผ่านหน้า Dashboard

ตรวจสอบ License ทุกครั้งที่ Server Restart?

ใช่ — ทุกครั้งที่ resource start จะมีการ verify กับ API ใหม่ทันที ไม่มี cache

โค้ดต้นฉบับปลอดภัยแค่ไหน?

ระบบ License ตรวจสอบ Key, Server IP และ Secret ทุกครั้งที่ resource start หาก License ไม่ผ่านและตั้ง stopOnFail = true — resource จะถูกหยุดทันที ป้องกันการใช้งานโดยไม่ได้รับอนุญาต

BuddyTH License System Docs — v2.0
shop.buddyth.com