# เชื่อม CXG-GTFAT กับ resource อื่น

ไฟล์ `bridge/*.lua` เป็นจุดที่แก้ไขได้เพื่อเชื่อมสิทธิ์ การบันทึกถาวร การแจ้งเตือน สถานี และรูปแบบข้อมูลรูปลักษณ์ patch สำหรับเมนูเป็นการแก้ไขด้วยตนเองสำหรับบางเวอร์ชัน resource จะไม่แก้ resource อื่นตอนติดตั้ง

## เมนูรูปลักษณ์

มี patch ที่เตรียมไว้สำหรับ repository และ revision ต่อไปนี้:

| เมนู | revision ต้นทาง | สิ่งที่ต้องทำเพิ่ม |
| --- | --- | --- |
| illenium-appearance | 1b003ec169b15f145d519438ba7c6454bf746f27 | ใช้ patch Lua สำหรับบันทึก component |
| fivem-appearance | b06da32881ed49042909b38a778c10dd9bd9eaed | ใช้ patch และสร้าง TypeScript bundle ของเกมใหม่ด้วยขั้นตอน `build:game` |
| qb-clothing | 8cca4009dd473ab30c30e443ecad50830b816ed7 | ใช้ patch ที่จุดบันทึก skin และ outfit |
| esx_skin / skinchanger | fe59ca0bd6da59e2ec6eb4a8d06ece312e96ae7a | path ของ patch อ้างอิงจากโฟลเดอร์รากของ repository esx_core |

โฟลเดอร์ `integrations` ในแพ็กเกจมีไฟล์ `illenium-appearance.patch`, `fivem-appearance.patch`, `qb-clothing.patch` และ `skinchanger.patch` ซึ่งจะไม่ถูกใช้อัตโนมัติ ใน checkout สำหรับพัฒนาเมนู ให้ตรวจ patch ที่ตรงกันก่อนใช้:

```sh
git apply --check /ruta/al/parche-correcto.patch
git apply /ruta/al/parche-correcto.patch
```

หากตรวจไม่ผ่าน ให้หยุดและปรับการเปลี่ยนแปลงให้เข้ากับเวอร์ชันที่ติดตั้ง อย่าฝืนใช้ patch fork และ revision ที่ใหม่กว่าอาจแตกต่างกัน patch ของ fivem-appearance ต้องสร้าง bundle ใหม่ด้วยขั้นตอน `build:game` ตามคำแนะนำของเมนูนั้น patch เหล่านี้ไม่ได้รับรองทุกเวอร์ชันและใช้แทนการทดสอบในเซิร์ฟเวอร์ไม่ได้

ขณะบันทึก ระบบจะล้าง technical decal เพื่อไม่ให้เมนูบันทึกเป็นตัวเลือกปกติของผู้เล่น เมนูที่มีตารางรูปลักษณ์ของตนเองต้องเรียกการล้างนี้ ณ จุดบันทึก น้ำหนักจะจัดเก็บแยกจากชุด

## บันทึกรูปลักษณ์ที่กำหนดเอง

client export `SanitizeAppearance` คืนค่าสำเนาของรูปลักษณ์ รูปแบบที่รองรับ ได้แก่ components, qb และ esx:

```lua
local appearanceCopy = exports['CXG_GTFAT']:SanitizeAppearance(
    ped,
    appearance,
    'components'
)

GuardarApariencia(appearanceCopy)
```

`GuardarApariencia` เป็นตัวอย่างการเรียก ให้แทนด้วยฟังก์ชันบันทึกของเมนู components รองรับ array ของ component หรือ object ที่มี property `components`; แต่ละ component ใช้ `component_id`, `drawable`, `texture` และอาจมี `palette` รูปแบบ qb ใช้ field `decals` ที่มี `item` และ `texture`; esx ใช้ `decals_1` และ `decals_2` export คืนค่าสำเนาแบบลึกและเก็บ field อื่นไว้ โดยไม่เปลี่ยน ped ชั่วคราว หาก adapter ของรูปแบบล้มเหลวหรือไม่รู้จักรูปแบบ จะคืนสำเนาเดิมและบันทึกข้อมูลวินิจฉัย

## ลงทะเบียน preview ped

หากเมนูใช้ ped สำหรับดูตัวอย่างที่แยกจากตัวละคร ให้ลงทะเบียน preview ของตัวละครที่ใช้อยู่ ยกเลิกการลงทะเบียนก่อนลบ entity:

```lua
local previewPed = ObtenerPreviewActual()

if previewPed and DoesEntityExist(previewPed) then
    exports['CXG_GTFAT']:RegisterPreviewPed(previewPed)
end

-- Al cerrar o cancelar el menú, antes de eliminar el ped:
exports['CXG_GTFAT']:UnregisterPreviewPed(previewPed)
EliminarPreview(previewPed)
```

`ObtenerPreviewActual` และ `EliminarPreview` เป็นตัวอย่างการเรียก ให้แทนด้วยฟังก์ชันของเมนู ลงทะเบียนเฉพาะ preview ของตัวละครปัจจุบัน ไม่ใช่ ped ในโลกหรือ preview ของตัวละครอื่น อีกทางเลือกคือกำหนด `Config.GetPreviewPed` ให้คืนค่า ped ปัจจุบันหรือ nil `RefreshAppearance(previewPed)` ขอให้ประเมิน ped ที่จัดการอยู่ใหม่ แต่ไม่ได้เปิด Fat เอง

## export สำหรับ resource ฝั่งเซิร์ฟเวอร์

server export เป็น API ที่มีสิทธิ์สูงสำหรับ resource ฝั่งเซิร์ฟเวอร์อื่น อย่าส่งต่อโดยตรงจาก event ที่ไคลเอนต์เรียกได้

```lua
local playerSource = source -- ID del jugador en el servidor
local kg, err = exports['CXG_GTFAT']:GetWeight(playerSource)
if kg == nil then
    print('No hay un peso confirmado:', err)
end

local appliedKg, setError = exports['CXG_GTFAT']:SetWeight(playerSource, 92.5)
if appliedKg == nil then
    print('No se pudo fijar el peso:', setError)
end

local addedKg, addError = exports['CXG_GTFAT']:AddWeight(playerSource, -2.0)
if addedKg == nil then
    print('No se pudo sumar peso:', addError)
end

local resetKg, resetError = exports['CXG_GTFAT']:ResetWeight(playerSource)
if resetKg == nil then
    print('No se pudo restablecer el peso:', resetError)
end
```

อาร์กิวเมนต์ `source` คือ ID ผู้เล่นบนเซิร์ฟเวอร์ ไม่ใช่ค่าที่ไคลเอนต์ส่งมา ทุกค่าเป็น kg `GetWeight` คืนค่าน้ำหนักหรือ nil, error `SetWeight` และ `AddWeight` คืนค่าน้ำหนักที่ใช้แล้วซึ่งปรับให้อยู่ในมาตรฐานเป็น kg หรือ nil, error; `SetWeight` ปฏิเสธค่านอกช่วงและปัดเป็นขั้นใกล้ที่สุด โดยกรณีกึ่งกลางปัดขึ้น `AddWeight` รับการเพิ่มค่าติดลบได้ `ResetWeight` บันทึก `defaultKg` และคืนค่าน้ำหนักที่ใช้แล้วเป็น kg ด้วย แต่ไม่ลบค่าที่จัดเก็บ

เปิด UI ให้ผู้เล่นจาก resource ฝั่งเซิร์ฟเวอร์อื่นได้ดังนี้:

```lua
local playerSource = source -- ID del jugador en el servidor
local opened, err = exports['CXG_GTFAT']:OpenWeightUI(playerSource)
if not opened then
    print('No se pudo abrir la báscula:', err)
end
```

ส่ง station ID ซึ่งไม่บังคับเป็นอาร์กิวเมนต์ที่สองได้: `OpenWeightUI`(source, 'gimnasio') หากไม่ระบุสถานี จะใช้ policy การเข้าถึงของคำสั่ง แม้ปิดการลงทะเบียนคำสั่งไว้ ผลลัพธ์ true ยืนยันว่าได้รับอนุญาตและส่งคำขอเปิดแล้ว ไม่ได้ยืนยันว่าไคลเอนต์แสดง UI แล้ว

### server export

| Export | การใช้งาน |
| --- | --- |
| `GetWeight(source)` | คืน kg หรือ nil, error |
| `SetWeight(source, kg)` | ตั้งน้ำหนักที่ตรวจสอบแล้วและคืน kg ที่ใช้ หรือ nil, error |
| `AddWeight(source, deltaKg)` | เพิ่มค่าความต่างและคืน kg ที่ใช้ หรือ nil, error |
| `ResetWeight(source)` | บันทึกและคืนค่าน้ำหนักเริ่มต้นที่ตั้งไว้เป็น kg หรือ nil, error |
| `GetWeightSettings()` | คืนสำเนาขีดจำกัดที่ใช้อยู่ |
| `OpenWeightUI(source, stationId?)` | ขอเปิดเครื่องชั่งภายใต้ policy ที่ใช้ |
| `RefreshCharacter`(source) | โหลด identity และน้ำหนักใหม่หลัง framework เปลี่ยนเป็นตัวละครที่ต้องการ |
| `ReconcileStorage`(source) | ปรับข้อมูลจัดเก็บหลังสถานะไม่แน่นอน เมื่อ adapter ยืนยันการเขียนก่อนหน้าได้ |

`RefreshCharacter` ไม่บังคับสำหรับระบบหลายตัวละคร ให้เรียกหลัง framework ทำให้ identity ที่ต้องการพร้อมใช้งาน resource ไม่ได้เชื่อม Qbox ให้อัตโนมัติ

ข้อผิดพลาดที่อาจพบ: `not_ready`, `invalid_source`, `player_unavailable`, `character_unavailable`, `invalid_weight`, `out_of_range`, `permission_denied`, `context_denied`, `busy`, `storage_failed`, `storage_timeout`, `storage_unknown` หรือ `not_synced` อย่าถือว่าข้อผิดพลาดด้านการจัดเก็บคือน้ำหนักที่ยืนยันแล้ว และอย่าลองเขียนซ้ำอัตโนมัติเมื่อได้ `storage_timeout` หรือ `storage_unknown`

## client export

```lua
local kg, err = exports['CXG_GTFAT']:GetWeight()
local settings = exports['CXG_GTFAT']:GetWeightSettings()
local isOpen = exports['CXG_GTFAT']:IsWeightUIOpen()
local closed = exports['CXG_GTFAT']:CloseWeightUI()
local status, statusError = exports['CXG_GTFAT']:GetFatStatus()
local reserved, reservedError = exports['CXG_GTFAT']:GetReservedDecals(PlayerPedId())
```

`GetWeight` คืนค่า nil, '`not_synced`' จนกว่าจะได้รับค่าที่ยืนยันจากเซิร์ฟเวอร์ `GetFatStatus(ped?)` และ `GetReservedDecals(ped)` ให้ข้อมูลแบบอ่านอย่างเดียว ไม่ได้ยืนยันว่า geometry แสดงผลแล้ว `GetReservedDecals` ใช้ซ่อน decal ที่สงวนไว้จากเมนูได้ `RefreshAppearance(previewPed?)` อัปเดตเฉพาะรูปลักษณ์ที่จัดการของผู้เล่นหรือ preview ที่ลงทะเบียนไว้

`RegisterPreviewPed(ped)` และ `UnregisterPreviewPed(ped)` ให้เมนูส่ง entity preview ให้ selector ได้ มีเพียง resource ที่ลงทะเบียน ped เท่านั้นที่ยกเลิกการลงทะเบียนได้ preview ที่ลงทะเบียนจะถูกล้างเมื่อ resource เจ้าของหยุดทำงาน

`SanitizeAppearance(ped, appearance, format)` คืนสำเนาของรูปลักษณ์; nil, error ไม่ใช่ตัวบอกความล้มเหลว `RegisterPreviewPed` และ `UnregisterPreviewPed` คืน true เมื่อทำงานสำเร็จ และคืน false หากลงทะเบียนหรือลบ ped ไม่ได้

## bridge ที่แก้ไขได้

| ไฟล์ | การปรับใช้ |
| --- | --- |
| `bridge/server.lua` | `CanAccess`, identity, การโหลด/บันทึกน้ำหนัก และ server hook |
| `bridge/client.lua` | preview ped, การแจ้งเตือน และ hook การเปลี่ยนน้ำหนัก/หน้าจอ |
| `bridge/interaction.lua` | ลงทะเบียนสถานีกับ target หรือแทนที่ TextUI |
| `bridge/appearance.lua` | อ่าน/เขียน decal ในรูปแบบรูปลักษณ์เฉพาะ |

โดยค่าเริ่มต้น `CanAccess` รู้จักเฉพาะ ace และ everyone สำหรับ jobs, groups หรือ custom ให้ใช้ API จริงของเซิร์ฟเวอร์เขียนการตรวจสอบ และคืนค่า true เท่านั้นเมื่ออนุญาต กรณี exception และค่าอื่นจะปฏิเสธ ฟิลด์ policy ของ job, group และ custom จะถูกตีความโดย adapter

ผู้ให้บริการจัดเก็บแบบกำหนดเองต้องใช้ signature `LoadWeight`(`characterId`, context, done), `SaveWeight`(`characterId`, kg, context, done) และหากต้องปรับการเขียนที่ไม่แน่นอน ให้ใช้ `ReconcileWeight`(`characterId`, context, done) การโหลดเรียก done(true, kg) (ใช้ kg = nil เมื่อไม่มีข้อมูล) หรือ done(false, '`storage_failed`') การบันทึกเรียก done(true) หลังยืนยันแล้วเท่านั้น มิฉะนั้นเรียก done(false, '`storage_failed`') การปรับข้อมูลเรียก done(true, true) เฉพาะเมื่อมั่นใจว่าไม่มีการเขียนก่อนหน้าที่จะเสร็จภายหลัง หากรับรองไม่ได้ ให้เรียก done(false, '`storage_unknown`') สำหรับการบันทึกแบบ asynchronous จริง ให้ตั้ง `AsyncStorage` = true; callback ต้องทำงานเสร็จในบริบท FiveM ที่รองรับการรอ API ให้เวลาได้สูงสุด 10 วินาทีก่อนแจ้ง timeout ห้ามลองซ้ำอัตโนมัติหลัง `storage_timeout`; ให้ปรับข้อมูลผ่านผู้ให้บริการที่รับรองว่าการเขียนก่อนหน้าจะไม่ถูกนำไปใช้ภายหลัง ห้ามกำหนดว่า asynchronous หากผู้ให้บริการคืนค่าก่อนเริ่มหรือจัดคิวการเขียน

hook `OnWeightChanged`, `OnCharacterChanged`, `OnUIOpened`, `OnUIClosed` และ Log ใช้สังเกตหรือบันทึกการเปลี่ยนแปลง hook ไม่ได้ให้สิทธิ์และไม่ย้อนการดำเนินการที่ยืนยันแล้ว

## ตรวจสอบก่อนเปิดใช้งาน

ต้องตรวจ patch และ adapter กับ resource และเวอร์ชันที่ใช้จริงบนเซิร์ฟเวอร์ ก่อนเปิดระบบให้ผู้เล่น:

1. ทดสอบการเปลี่ยน Normal/Fat ซ้ำด้วยตัวละคร freemode ชายและหญิง
2. ขณะเปิด Fat ให้บันทึกและโหลดรูปลักษณ์กับชุดอีกครั้ง ยืนยันว่า technical decal ไม่ถูกบันทึกเป็นตัวเลือกปกติ และน้ำหนักยังแยกจากชุด
3. เมื่อเปิดการบันทึกแยกตามตัวละคร ให้รีสตาร์ต resource และเชื่อมต่อใหม่ ยืนยันว่าตัวละครเดิมได้น้ำหนักคืน และตัวละครอื่นไม่รับค่านั้นไป
4. เปิดเมนูรูปลักษณ์โดยลงทะเบียน preview แล้วตรวจว่าเป็นตัวละครปัจจุบัน เมื่อยกเลิกหรือปิดเมนู ให้ยกเลิกการลงทะเบียนก่อนลบ entity

ต้องตรวจสอบบนเซิร์ฟเวอร์ของคุณเพื่อยืนยันชุดเมนู framework และเสื้อผ้าที่ใช้จริง การมี patch ไม่ได้ยืนยันผลลัพธ์เหล่านี้ด้วยตัวเอง
