FiveM State Bag Change Handler คืออะไร? วิธีใช้ AddStateBagChangeHandler

 FiveM State Bag Change Handler คือระบบสำหรับตรวจจับการเปลี่ยนแปลงของ State Bag แบบ Event-driven โดยใช้ AddStateBagChangeHandler() ทำให้ Script สามารถทำงานทันทีเมื่อ State ของ Entity, Player หรือ State Bag ที่ตรงกับ Filter เปลี่ยน โดยไม่ต้องสร้าง CreateThread() เพื่อ Poll ค่าซ้ำ ๆ ตลอดเวลา

ตัวอย่างเช่น Server กำหนด State ของ Vehicle

Entity(vehicle).state:set(
    'garage:locked',
    true,
    true
)

Client สามารถติดตามการเปลี่ยนแปลงของ Key นี้ด้วย

AddStateBagChangeHandler(
    'garage:locked',
    nil,
    function(
        bagName,
        key,
        value,
        reserved,
        replicated
    )

        print(
            'Lock state:',
            value
        )

    end
)

เมื่อ State เปลี่ยน Handler จะทำงาน

Concept คือ

State เปลี่ยน
↓
FiveM ตรวจพบ
↓
AddStateBagChangeHandler
↓
เรียก Handler
↓
Resource ตอบสนองต่อ State ใหม่

นี่ทำให้ State Bags ทรงพลังมากกว่าการเป็นเพียงพื้นที่เก็บ Key/Value เพราะ Resource สามารถสร้างระบบ Reactive ที่ทำงานเฉพาะเมื่อข้อมูลเปลี่ยนได้

① AddStateBagChangeHandler คืออะไร

AddStateBagChangeHandler() เป็น CFX Native สำหรับ Register Handler ที่ติดตามการเปลี่ยน State Bag

Syntax คือ

AddStateBagChangeHandler(
    keyFilter,
    bagFilter,
    handler
)

ประกอบด้วย

keyFilter
→ สนใจ Key ไหน

bagFilter
→ สนใจ State Bag ไหน

handler
→ Function ที่ทำงานเมื่อ State เปลี่ยน

จึงสามารถ Filter ได้ทั้งตาม Key และ State Bag

② ตัวอย่างพื้นฐานที่สุด

สมมติต้องการติดตาม Key

isDead

สามารถเขียน

AddStateBagChangeHandler(
    'isDead',
    nil,
    function(bagName, key, value)

        print(
            bagName,
            key,
            value
        )

    end
)

nil ที่ bagFilter หมายถึงไม่ได้จำกัด State Bag ใดเป็นพิเศษ

ดังนั้น Handler สนใจ Key isDead ที่ตรงกับ Filter

③ Callback มี Parameter อะไรบ้าง

Handler เต็มรูปแบบรับ

function(
    bagName,
    key,
    value,
    reserved,
    replicated
)

แต่ละค่าคือ

bagName
→ State Bag ที่เปลี่ยน

key
→ Key ที่เปลี่ยน

value
→ ค่าใหม่

reserved
→ ปัจจุบันไม่ได้ใช้

replicated
→ การ Set นี้ถูกกำหนดให้ Replicate หรือไม่

Developer ไม่จำเป็นต้องใช้ทุก Parameter

④ bagName คืออะไร

bagName เป็น Internal State Bag ID

รูปแบบทั่วไปที่เอกสาร Cfx.re ระบุ เช่น

player:Source
entity:NetID
localEntity:Handle

ตัวอย่าง

player:15

อาจหมายถึง Player State Bag

หรือ

entity:81

หมายถึง Network Entity ที่เกี่ยวข้องกับ Network ID นั้น

แต่โดยทั่วไปไม่ควร Parse String เองถ้ามี Native สำหรับ Resolve Bag อยู่แล้ว

⑤ GetEntityFromStateBagName คืออะไร

ถ้า bagName เป็น Entity State Bag สามารถใช้

local entity =
    GetEntityFromStateBagName(
        bagName
    )

Native นี้จะแปลง State Bag Name ไปเป็น Entity Handle ที่ใช้ได้ใน Context ปัจจุบัน

ตัวอย่าง

AddStateBagChangeHandler(
    'fuel',
    nil,
    function(bagName, key, value)

        local entity =
            GetEntityFromStateBagName(
                bagName
            )

        if entity == 0 then
            return
        end

        print(
            'Fuel:',
            value
        )

    end
)

⑥ ทำไมต้องตรวจ entity == 0

เพราะ State Handler สามารถทำงานในจังหวะที่ Client ยังไม่มี Entity ที่ Valid หรือ Entity อาจออกจาก Scope ไปแล้ว

ดังนั้น Cfx.re Example เองใช้

if entity == 0 then
    return
end

ก่อนทำ Entity Logic

อย่าสมมติว่า Handler ทำงานแล้ว Entity ต้องมีอยู่เสมอ

⑦ DoesEntityExist ยังควรตรวจไหม

ควรใน Logic ที่ Entity อาจหายระหว่างการรอ

เช่น

while not HasCollisionLoadedAroundEntity(
    entity
) do

    if not DoesEntityExist(entity) then
        return
    end

    Wait(250)
end

เพราะระหว่าง Wait() Entity อาจออกจาก Scope หรือถูกลบ

⑧ GetPlayerFromStateBagName คืออะไร

สำหรับ Player State Bag ใช้

local player =
    GetPlayerFromStateBagName(
        bagName
    )

ถ้า State Bag นั้นเป็นของ Player จะได้ Player Handle ที่เหมาะกับ Context

เอกสาร Cfx.re ระบุว่า

Server
→ คืน Player source

Client
→ คืน Player handle/index

ถ้า Bag ไม่ใช่ Player หรือ Player ไม่มีอยู่ จะคืน 0

⑨ ตัวอย่าง Player State Change

AddStateBagChangeHandler(
    'isDead',
    nil,
    function(bagName, key, value)

        local player =
            GetPlayerFromStateBagName(
                bagName
            )

        if player == 0 then
            return
        end

        print(
            'Player:',
            player,
            'Dead:',
            value
        )

    end
)

เหมาะกับ State เช่น

isDead
onDuty
isCuffed
isBusy

ตาม Architecture ของ Resource

⑩ keyFilter คืออะไร

keyFilter ใช้จำกัดว่า Handler สนใจ State Key ใด

ตัวอย่าง

AddStateBagChangeHandler(
    'vehicle:fuel',
    nil,
    function(...)
    end
)

Handler นี้สนใจเฉพาะ

vehicle:fuel

แทนการรับ State Changes ทุก Key แล้วค่อยตรวจเองภายใน Function

⑪ keyFilter เป็น nil ได้ไหม

ได้

nil หมายถึงไม่มี Key Filter

Concept

AddStateBagChangeHandler(
    nil,
    nil,
    function(...)
    end
)

จะเป็น Handler กว้างมาก เพราะไม่ได้จำกัด Key หรือ Bag

ไม่ควรใช้ Global Listener แบบกว้างโดยไม่มีเหตุผล โดยเฉพาะ Server ใหญ่ที่ State เปลี่ยนบ่อย

⑫ ทำไมควรใช้ keyFilter

สมมติ Server มี State Keys จำนวนมาก

fuel
locked
engine
job
duty
cuffed
dead
mission
status

แต่ Resource ของคุณสนใจเพียง

fuel

ควร Filter ตั้งแต่ Registration

AddStateBagChangeHandler(
    'fuel',
    nil,
    handler
)

ไม่จำเป็นต้องรับทุก Change แล้วเขียน if key ~= 'fuel' then return end ตลอด

⑬ bagFilter คืออะไร

bagFilter ใช้กรอง State Bag เฉพาะ Bag

ตัวอย่าง Internal Bag ID อาจเป็น

entity:125

ถ้าต้องการติดตาม State ของ Entity หนึ่งโดยเฉพาะ สามารถใช้ Bag Filter ที่ตรงกับ Entity State Bag นั้น

แต่ในระบบทั่วไป การ Filter ตาม Key แล้ว Resolve Entity ใน Handler มักยืดหยุ่นกว่า

⑭ bagFilter เป็น nil หมายถึงอะไร

หมายถึงไม่จำกัดว่า Key ต้องมาจาก Bag ไหน

ตัวอย่าง

AddStateBagChangeHandler(
    'vehicle:fuel',
    nil,
    function(...)
    end
)

หมายความว่า Resource สนใจการเปลี่ยน vehicle:fuel ของ State Bags ที่ตรงตามระบบ ไม่ได้ Fix เฉพาะ Vehicle คันเดียว

เหมาะกับ Vehicle Fuel System

⑮ value คืออะไร

value คือ ค่าใหม่ ที่กำลังถูก Set ให้ State Key

ตัวอย่าง

เดิม:
locked = false

เปลี่ยนเป็น:
locked = true

Handler จะได้รับ

value == true

จึงสามารถทำ

if value then
    -- lock
else
    -- unlock
end

⑯ ถ้าลบ State Key value เป็นอะไร

State Bag มักลบ Key ด้วยการ Set เป็น

nil

ดังนั้น Handler อาจได้รับ

value = nil

Resource ต้องออกแบบ Handler รองรับกรณี State ถูก Clear

ตัวอย่าง

if value == nil then
    -- cleanup
    return
end

⑰ ค่าเก่ายังอ่านได้ใน Handler หรือไม่

นี่เป็นรายละเอียดสำคัญ

เอกสาร Native ระบุว่า ระหว่าง Handler ทำงาน ค่าเก่ายังคงอยู่ใน State Bag

ส่วน Parameter

value

คือค่าใหม่

ดังนั้นใน Handler สามารถมี Concept

State Bag ปัจจุบัน
→ old value

value parameter
→ new value

นี่มีประโยชน์ถ้าต้องเปรียบเทียบก่อน/หลัง

⑱ ตัวอย่างอ่าน Old Value

Concept เช่น

AddStateBagChangeHandler(
    'fuel',
    nil,
    function(bagName, key, value)

        local oldValue =
            GetStateBagValue(
                bagName,
                key
            )

        print(
            'Old:',
            oldValue,
            'New:',
            value
        )

    end
)

ต้องจำว่า Behavior นี้เป็นรายละเอียดของ Callback Timing

หลัง Handler จบ State จะเข้าสู่ค่าที่เปลี่ยนตามระบบ

⑲ reserved คืออะไร

Parameter ตัวที่ 4 คือ

reserved

เอกสารปัจจุบันระบุว่า

Currently unused

ดังนั้น Lua Script มักเขียน

function(
    bagName,
    key,
    value,
    _,
    replicated
)

ใช้ _ เพื่อบอกว่าไม่สนใจ Parameter นี้

⑳ replicated คืออะไร

replicated บอกว่าการ Set State ครั้งนั้นถูกกำหนดให้ Replicate หรือไม่

เช่น Client อาจ Set

Entity(entity).state:set(
    'taskAck',
    true,
    true
)

ค่า true สุดท้ายคือ Request ให้ Replicate

Handler สามารถดู replicated เพื่อใช้ประกอบ Logic ได้

แต่ไม่ควรตีความว่า replicated=true แปลว่าข้อมูลนั้น “เชื่อถือได้”

㉑ replicated=true ไม่ใช่ Security Proof

นี่สำคัญ

replicated = true

หมายถึง State Change ถูก Set ในรูปแบบ Replicated

ไม่ได้หมายถึง

ข้อมูลมาจาก Trusted Server

เสมอไป

เพราะตาม Default State Bag Policy Client Owner สามารถเขียน State บางประเภทได้

Security-critical Data ยังต้องมี Server Authority

㉒ Change Handler ปฏิเสธ State Change ได้ไหม

ไม่ได้ในปัจจุบัน

เอกสาร AddStateBagChangeHandler ระบุว่า Handler ไม่สามารถเลือก Reject State Change ได้

ดังนั้นอย่าเขียน Handler แล้วคิดว่า

if invalid then
    return
end

จะยกเลิกการเปลี่ยน State

มันเพียงหยุด Logic ของ Handler

ไม่ใช่ Cancel State Change

㉓ ถ้าต้องป้องกัน Client แก้ State ทำอย่างไร

ต้องควบคุม Authority ตั้งแต่ State Write Policy

เช่น

  • Server เป็นผู้ Set State สำคัญ

  • Validate Client Requests

  • จำกัด Resource Architecture

  • ใช้ sv_stateBagStrictMode หากเหมาะกับ Server

เมื่อ Strict Mode เปิดตามเอกสารปัจจุบัน จะให้เฉพาะ Server แก้ State Bags ได้

ต้องตรวจ Compatibility ของ Resource ก่อนใช้งาน

㉔ Change Handler ใช้ Client ได้ไหม

ได้

Use Case ที่พบบ่อยมากคือ Server Set Entity State แล้ว Client ตอบสนอง

Server
↓
Set Entity State
↓
Replication
↓
Client Handler
↓
Native / Visual Action

เช่น

  • Lock Vehicle

  • Apply Vehicle Damage

  • Initialize Entity

  • Cuff Ped

  • Toggle Invincibility

  • Update NUI

㉕ Change Handler ใช้ Server ได้ไหม

ได้

State Bag Change Handler เป็น API ที่ใช้กับ State Awareness และสามารถมี Handler ฝั่ง Server ได้

ตัวอย่าง Server อาจติดตาม Player State

AddStateBagChangeHandler(
    'job:onDuty',
    nil,
    function(
        bagName,
        key,
        value,
        reserved,
        replicated
    )

        local src =
            GetPlayerFromStateBagName(
                bagName
            )

        if src == 0 then
            return
        end

        print(
            src,
            value
        )

    end
)

แต่ State ที่ Client เขียนต้องถูกมองเป็น Untrusted Input ตาม Security Requirement

㉖ State Bag Handler ต่างจาก Event Handler อย่างไร

Event Handler

AddEventHandler(
    'vehicle:stored',
    function(...)
    end
)

ทำงานเมื่อ Event ถูก Trigger

State Bag Change Handler

AddStateBagChangeHandler(
    'vehicle:locked',
    nil,
    function(...)
    end
)

ทำงานเมื่อ State เปลี่ยน

จำง่าย ๆ ว่า

Event Handler
→ ฟังเหตุการณ์

State Bag Handler
→ ฟังสถานะเปลี่ยน

㉗ State Bag Handler ต่างจาก Polling อย่างไร

Polling

CreateThread(function()
    while true do
        Wait(500)

        local value =
            Entity(vehicle).state.locked

        -- check
    end
end)

ต้องตรวจซ้ำแม้ค่าไม่เปลี่ยน

State Bag Handler

locked เปลี่ยน
↓
Handler ทำงาน

จึงเหมาะกับข้อมูลที่เปลี่ยนไม่บ่อย

㉘ Change Handler ช่วย Performance ได้อย่างไร

สมมติ Player อยู่ Server 4 ชั่วโมง

Polling ทุก 500 ms จะตรวจประมาณ

8 ครั้ง / 4 วินาที

ต่อเนื่องตลอด Session

แต่ถ้า Vehicle Lock เปลี่ยนเพียง 20 ครั้ง Handler ก็ทำงานเฉพาะตอนที่ State Change ถูกส่งมา

จึงลดงานซ้ำใน Use Case ที่เป็น Reactive State ได้มาก

อย่างไรก็ตาม Handler ที่ทำงานเองก็ต้อง Optimize เช่นกัน

㉙ Handler หนักเกินไปได้ไหม

ได้

ตัวอย่าง

AddStateBagChangeHandler(
    'fuel',
    nil,
    function(...)

        -- Query ใหญ่
        -- Loop Entities หลายพันตัว
        -- Heavy Native Calls

    end
)

ถ้า State เปลี่ยนถี่ Handler ก็สามารถสร้าง Load ได้

Reactive ไม่ได้หมายความว่า Cost เป็นศูนย์

㉚ อย่า Update State ทุก Frame แล้วหวังว่า Handler จะ Optimize

ตัวอย่าง Server/Owner ทำ

CreateThread(function()
    while true do
        Wait(0)

        Entity(vehicle).state:set(
            'fuel',
            fuel,
            true
        )
    end
end)

แล้ว Client ใช้ Change Handler

Resource ก็ยังสร้าง State Updates ทุก Tick

สิ่งที่ควรทำคือ Set เมื่อค่าเปลี่ยนอย่างมีความหมาย

เช่น

100
↓
99
↓
98

ตาม Interval/Threshold ที่เหมาะสม

ไม่จำเป็นต้อง Replicateค่าเดิมซ้ำทุก Frame

㉛ ตัวอย่าง Vehicle Lock Handler

Server

Entity(vehicle).state:set(
    'garage:locked',
    true,
    true
)

Client

AddStateBagChangeHandler(
    'garage:locked',
    nil,
    function(bagName, key, value)

        local vehicle =
            GetEntityFromStateBagName(
                bagName
            )

        if vehicle == 0 then
            return
        end

        SetVehicleDoorsLocked(
            vehicle,
            value and 2 or 1
        )

    end
)

Server ควบคุม State ส่วน Client Apply Native Behavior

㉜ แต่ Vehicle Lock Security อยู่ตรงไหน

Client Native Lock เป็น Gameplay Presentation/Behavior

สิทธิ์ในการเปลี่ยนจาก

locked = true

เป็น

locked = false

ควรถูกตรวจ Server-side หากเกี่ยวข้องกับ Ownership

Flow ที่ดีกว่า

Client ขอปลดล็อก
↓
Server ตรวจ Vehicle Ownership/Key
↓
Server Set State
↓
Clients Handler Apply Lock

㉝ ตัวอย่าง Player Duty Handler

Server

Player(src).state:set(
    'job:onDuty',
    true,
    true
)

Client Handler

AddStateBagChangeHandler(
    'job:onDuty',
    nil,
    function(bagName, key, value)

        local player =
            GetPlayerFromStateBagName(
                bagName
            )

        if player == 0 then
            return
        end

        print(
            'Duty changed:',
            value
        )

    end
)

Resource UI สามารถใช้ Handler อัปเดต Icon หรือ Marker ได้โดยไม่ต้อง Poll Job State ตลอดเวลา

㉞ ตัวอย่าง Handcuff State

Server

Player(target).state:set(
    'police:cuffed',
    true,
    true
)

Client Handler

AddStateBagChangeHandler(
    'police:cuffed',
    nil,
    function(bagName, key, value)

        local player =
            GetPlayerFromStateBagName(
                bagName
            )

        if player == 0 then
            return
        end

        local ped =
            GetPlayerPed(player)

        if ped == 0 then
            return
        end

        -- apply visual/gameplay state

    end
)

Server ยังควรเป็น Authority ว่า Player ถูกจับจริงหรือไม่

㉟ State Bag Handler กับ Entity Scope

Handler อาจถูกเรียกในความสัมพันธ์กับ State Replication ของ Entity ที่เข้าสู่ Scope

ดังนั้น Client ต้องเข้าใจว่า

State Bag มีข้อมูล

ไม่ได้แปลว่า

Entity Handle พร้อมใช้เสมอทันที

นี่เป็นเหตุผลที่เอกสาร Cfx.re ใช้ GetEntityFromStateBagName() และตรวจ entity == 0

㊱ Entity อาจยังโหลด Collision ไม่เสร็จ

Official Example ของ AddStateBagChangeHandler() ใช้

while not HasCollisionLoadedAroundEntity(
    entity
) do

    if not DoesEntityExist(entity) then
        return
    end

    Wait(250)
end

จากนั้นจึง Apply Native

เหมาะกับ State ที่ต้องทำงานกับ Entity ใน World หลัง Streaming พร้อม

㊲ ทำไมต้องเช็ก DoesEntityExist ระหว่าง Wait

เพราะขณะรอ Collision

Player เดินออกไป
↓
Entity ออกจาก Scope
↓
Local Entity หาย

ถ้า Thread ยังคงรอแล้วใช้ Entity Handle เดิม อาจเกิด Error/Behavior ผิด

จึงต้อง Check Lifecycle ระหว่าง Async Work

㊳ State Handler สามารถ Wait ได้ไหม

Lua Example ของ Cfx.re ใช้ Wait() ภายใน State Bag Handler ได้

เช่น

while not HasCollisionLoadedAroundEntity(
    entity
) do
    Wait(250)
end

แต่ต้องระวังว่า State อาจเปลี่ยนอีกรอบหรือ Entity Ownership/Scope เปลี่ยนระหว่างรอ

จึงต้อง Validate State อีกครั้งเมื่อจำเป็น

㊴ Ownership เปลี่ยนระหว่าง Handler ได้ไหม

ได้

โดยเฉพาะ Entity ที่เพิ่งสร้างและมี Clients หลายคนอยู่ใกล้

Cfx.re Migration Guide ระบุว่า Owner สามารถเปลี่ยนเร็วในช่วง Initialization

ดังนั้น Owner-dependent Handler ต้องตรวจ

NetworkGetEntityOwner(entity)

ในจังหวะที่กำลังทำ Owner-only Action จริง

㊵ ตัวอย่าง Owner-only Handler

AddStateBagChangeHandler(
    'vehicle:needsInit',
    nil,
    function(bagName, key, value)

        if not value then
            return
        end

        local entity =
            GetEntityFromStateBagName(
                bagName
            )

        if entity == 0 then
            return
        end

        if NetworkGetEntityOwner(
            entity
        ) ~= PlayerId() then
            return
        end

        SetVehicleOnGroundProperly(
            entity
        )

    end
)

เหมาะเมื่อ Native ต้องให้ Current Entity Owner เป็นผู้ Apply

㊶ ทำไม Owner-only Handler แบบ Return ครั้งเดียวอาจยังไม่พอ

เพราะในช่วง Entity Initialization

Handler เรียก Client A
↓
A ไม่ใช่ Owner
↓
Return
↓
ต่อมา A/B Ownership เปลี่ยน

ถ้าไม่มี Mechanism เพิ่ม State อาจยังไม่ถูก Process

Cfx.re Migration Guide จึงแสดง Pattern ที่ State คงอยู่จน Owner คนใดคนหนึ่งทำงานสำเร็จและ Clear State

㊷ Pattern Initialization ที่ Robust

Server

Entity(vehicle).state:set(
    'vehicle:needsInit',
    true,
    true
)

Client Handler

State = true
↓
Resolve Entity
↓
ตรวจ Current Owner
↓
ถ้ายังไม่ใช่
→ รอ/ลองตาม Lifecycle

ถ้าเป็น Owner
↓
Initialize
↓
Clear State

State ทำหน้าที่เป็น Pending Work Flag

㊸ GetStateBagValue คืออะไร

สามารถอ่าน Value จาก Bag โดยใช้

GetStateBagValue(
    bagName,
    key
)

มีประโยชน์ภายใน Handler โดยเฉพาะเมื่อต้องตรวจว่า State ยัง Pending อยู่หรือไม่หลัง Wait()

ตัวอย่าง

local current =
    GetStateBagValue(
        bagName,
        key
    )

if not current then
    return
end

㊹ ทำไมต้องอ่าน State ใหม่หลัง Wait

สมมติ Handler ได้

value = true

จากนั้นรอ 1 วินาที

ระหว่างนั้น Resource อื่นอาจ Set

value = false

ดังนั้นก่อนทำ Action สำคัญหลัง Async Delay ควรพิจารณา Re-read State

ไม่ควรถือว่า Parameter ตอนเริ่ม Handler ยังเป็น Current State เสมอไป

㊺ State Handler ถูกเรียกหลายครั้งได้ไหม

ได้

ทุกครั้งที่ State Change ตรงกับ Filter Handler สามารถถูกเรียกอีกครั้ง

ตัวอย่าง

fuel = 100
fuel = 90
fuel = 80
fuel = 70

Handler fuel จะตอบสนองต่อการ Update เหล่านั้นตามระบบ

ดังนั้น Handler ต้องรองรับ Repeated Calls

㊻ ระวังสร้าง Thread ซ้ำใน Handler

ตัวอย่างอันตราย

AddStateBagChangeHandler(
    'active',
    nil,
    function(bagName, key, value)

        if not value then
            return
        end

        CreateThread(function()
            while true do
                Wait(0)

                -- permanent logic
            end
        end)

    end
)

ถ้า State ถูก Set true หลายรอบ อาจสร้าง Infinite Threads หลายตัว

ควรมี Lifecycle/Guard ชัดเจน

㊼ Handler ควร Idempotent ไหม

หากทำได้ควรออกแบบให้ Calling ซ้ำไม่ทำให้ State พัง

ตัวอย่าง Set Vehicle Lock

SetVehicleDoorsLocked(
    vehicle,
    2
)

ทำซ้ำกับ State locked=true อาจไม่เสียหายมาก

แต่ Action อย่าง

Give Money
Give Item
Charge Account

ไม่ควรผูกกับ Client-side State Change Handler แบบไม่มี Transaction Protection

㊽ State Change Handler ใช้ทำ Reward ดีไหม

ไม่ควรใช้ Client Handler เป็น Authority ของ Economy

ตัวอย่างไม่ดี

State rewardReady = true
↓
Client Handler
↓
Client บอก Server Give Money

แล้ว Server เชื่อโดยไม่ตรวจ

Reward Eligibility ต้องอยู่ Server-side State/Business Logic

State Bag Handler ใช้เป็น Signal ได้ แต่ Server ยังต้อง Validate

㊾ Handler สามารถทำ NUI Update ได้ไหม

ได้ และเหมาะมาก

ตัวอย่าง Player Duty

job:onDuty เปลี่ยน
↓
Handler
↓
SendNUIMessage
↓
HUD Update

ไม่ต้องให้ HUD Poll Player State ทุก Frame

ตัวอย่าง

AddStateBagChangeHandler(
    'job:onDuty',
    nil,
    function(bagName, key, value)

        SendNUIMessage({
            action = 'duty',
            active = value
        })

    end
)

แต่ควร Filter ว่า State Bag เป็นของ Local Player หาก UI นี้ต้องแสดงเฉพาะตัวเอง

㊿ Local Player State Handler ต้อง Filter อย่างไร

แนวทางหนึ่งคือ Resolve Player จาก Bag แล้วตรวจว่าเป็น Local Player

local player =
    GetPlayerFromStateBagName(
        bagName
    )

if player == 0 then
    return
end

if player ~= PlayerId() then
    return
end

จากนั้นค่อย Update Local HUD

ช่วยไม่ให้ State ของ Player คนอื่นเปลี่ยนแล้ว UI ของเราเปลี่ยนตาม

51 GlobalState Change ติดตามได้ไหม

State Bag Change Handler ถูกออกแบบสำหรับ State Bag Changes และ GlobalState ก็เป็น State Bag Type หนึ่งในระบบ

หาก Resource ต้องตอบสนองต่อ Global State Change สามารถออกแบบ Handler ตาม Bag/Key ที่เกี่ยวข้องได้

ตัวอย่าง Use Case

doubleXP
eventActive
weatherMode
serverPhase

แต่ต้อง Filter ให้แคบเพื่อไม่รับ Global Changes ที่ไม่เกี่ยวข้อง

52 Change Handler กับ playerEnteredScope

OneSync Documentation เตือนว่าการใช้

playerEnteredScope
playerLeftScope

มี Scaling Cost ตามจำนวน Players ใน Scope

Cfx.re แนะนำ State Bags เมื่อ Use Case สามารถใช้ Scoped State Change แทนได้

ดังนั้นระบบ Player State หลายประเภทควรพิจารณา

Player State
+
AddStateBagChangeHandler

แทนการ Manage Scope Events เอง

53 ทำไม State Bag Scale ดีกว่าใน Use Case นี้

แทน

Player A เข้า Scope
↓
Event

Player B เข้า Scope
↓
Event

Player C เข้า Scope
↓
Event

และ Resource ต้อง Sync State เอง

State Bag สามารถให้ Relevant State ตาม State Awareness/Replication ของ Entity/Player

ช่วยลด Custom Scope Management

แต่ไม่ใช่ว่า State Bag จะเหมาะกับทุก Event

54 AddStateBagChangeHandler Return ค่าอะไร

Native นี้ Return ค่า Integer ซึ่งใช้เป็น Handler Cookie

Concept

local cookie =
    AddStateBagChangeHandler(
        'fuel',
        nil,
        function(...)
        end
    )

Cookie นี้สามารถใช้ระบุ Handler หากต้องการ Remove Handler

55 RemoveStateBagChangeHandler คืออะไร

FiveM มี

RemoveStateBagChangeHandler(
    cookie
)

สำหรับลบ State Bag Change Handler ที่ Register ไว้

ตัวอย่าง

local cookie =
    AddStateBagChangeHandler(
        'fuel',
        nil,
        handler
    )

-- เมื่อไม่ต้องการแล้ว
RemoveStateBagChangeHandler(
    cookie
)

อย่างไรก็ตาม เอกสาร Cfx.re ปัจจุบันทำเครื่องหมาย RemoveStateBagChangeHandler ว่า Experimental

จึงควรตรวจ Runtime Compatibility ก่อนพึ่ง API นี้ใน Production Logic สำคัญ

56 เมื่อไรควร Remove Handler

Handler ส่วนใหญ่ที่ต้องทำงานตลอด Resource Lifetime ไม่จำเป็นต้องสร้าง/ลบซ้ำตลอดเวลา

แต่ Dynamic Handler อาจเหมาะกับการ Remove เช่น

  • Temporary Mission

  • Debug System

  • Temporary Resource Feature

  • Runtime Module Enable/Disable

อย่าสร้าง Handler ใหม่ทุกครั้งที่เปิด Menu ถ้า Handler ถาวรตัวเดียวก็ทำงานได้

57 State Handler ซ้ำมีผลอะไร

ถ้า Register Handler เดิมหลายครั้ง

Handler 1
Handler 2
Handler 3

เมื่อ State เปลี่ยน Logic อาจทำงานซ้ำหลายรอบ

เช่น NUI Update ซ้ำ, Native Apply ซ้ำ หรือ Thread ถูกสร้างซ้ำ

ดังนั้นควรวาง Registration ใน Initialization ที่ชัดเจน

58 วิธี Debug AddStateBagChangeHandler

เริ่มจาก Handler ที่ Print ทุก Parameter ที่ต้องการ

AddStateBagChangeHandler(
    'debug:test',
    nil,
    function(
        bagName,
        key,
        value,
        reserved,
        replicated
    )

        print(
            'bag:',
            bagName
        )

        print(
            'key:',
            key
        )

        print(
            'value:',
            value
        )

        print(
            'replicated:',
            replicated
        )

    end
)

จากนั้นค่อยเพิ่ม

GetEntityFromStateBagName()

หรือ

GetPlayerFromStateBagName()

เพื่อหาว่า Bag เป็นของอะไร

59 Checklist ก่อนใช้ State Bag Change Handler

ตรวจอย่างน้อย

① State นี้ควรใช้ Handler แทน Polling หรือไม่?
② keyFilter แคบพอหรือไม่?
③ ต้องใช้ bagFilter หรือไม่?
④ bagName เป็น Entity, Player หรือ Global?
⑤ Resolve ด้วย Native ที่ถูกต้องหรือไม่?
⑥ ตรวจ entity/player == 0 หรือยัง?
⑦ Handler อาจถูกเรียกหลายครั้งหรือไม่?
⑧ Handler ทำ Heavy Logic หรือไม่?
⑨ Handler มี Wait หรือ Async หรือไม่?
⑩ หลัง Wait ต้องอ่าน State ใหม่ไหม?
⑪ Entity อาจออกจาก Scope หรือไม่?
⑫ ต้องตรวจ DoesEntityExist ซ้ำไหม?
⑬ Ownership เปลี่ยนได้ไหม?
⑭ Logic ต้องทำเฉพาะ Owner หรือไม่?
⑮ value=nil ถูก Handle หรือยัง?
⑯ State นี้ Client เขียนได้หรือไม่?
⑰ Security-critical หรือไม่?
⑱ replicated ถูกตีความถูกหรือไม่?
⑲ Handler ถูก Register ซ้ำหรือไม่?
⑳ ต้อง Remove Handler จริงไหม?

⑥⓪ ตัวอย่าง State Bag Change Handler แบบสมบูรณ์

สมมติ Server สร้าง Vehicle แล้วต้องการให้ Client Owner ทำ Initialization

server.lua

local function createMissionVehicle(
    model,
    coords
)

    local vehicle =
        CreateVehicleServerSetter(
            joaat(model),
            'automobile',
            coords.x,
            coords.y,
            coords.z,
            coords.w
        )

    if vehicle == 0 then
        return nil
    end

    SetEntityOrphanMode(
        vehicle,
        2
    )

    Entity(vehicle).state:set(
        'mission:needsInit',
        true,
        true
    )

    return vehicle
end

client.lua

AddStateBagChangeHandler(
    'mission:needsInit',
    nil,
    function(
        bagName,
        key,
        value,
        reserved,
        replicated
    )

        if not value then
            return
        end

        local timeout =
            GetGameTimer() + 5000

        while true do
            Wait(0)

            local currentState =
                GetStateBagValue(
                    bagName,
                    key
                )

            if not currentState then
                return
            end

            local entity =
                GetEntityFromStateBagName(
                    bagName
                )

            if entity ~= 0
            and DoesEntityExist(entity)
            and NetworkGetEntityOwner(
                entity
            ) == PlayerId() then

                SetVehicleOnGroundProperly(
                    entity
                )

                Entity(entity).state:set(
                    'mission:needsInit',
                    nil,
                    true
                )

                return
            end

            if GetGameTimer() >
                timeout then

                return
            end
        end

    end
)

Flow คือ

Server
↓
Create Networked Vehicle
↓
needsInit = true
↓
State Replication
↓
Client Handler
↓
Resolve Entity
↓
ตรวจ Current Owner
↓
Owner ทำ Initialization
↓
Clear State

นี่เป็นตัวอย่างสำคัญของ State Bag Change Handler เพราะไม่ผูก Initialization ไว้กับ Client คนใดคนหนึ่งถาวร

คำถามที่พบบ่อยเกี่ยวกับ FiveM State Bag Change Handler

FiveM State Bag Change Handler คืออะไร

คือ Handler ที่ทำงานเมื่อ State Bag Key ที่ตรงกับ Filter เปลี่ยน

ใช้ Function อะไร

ใช้

AddStateBagChangeHandler(
    keyFilter,
    bagFilter,
    handler
)

keyFilter คืออะไร

ใช้จำกัด State Key ที่ต้องการติดตาม

bagFilter คืออะไร

ใช้จำกัด State Bag ที่ต้องการติดตาม

ใส่ nil ที่ bagFilter ได้ไหม

ได้ หมายถึงไม่จำกัด Bag เฉพาะตัว

Handler รับค่าอะไร

หลัก ๆ คือ

bagName
key
value
reserved
replicated

value เป็นค่าเก่าหรือค่าใหม่

value คือค่าใหม่ ส่วนเอกสาร Native ระบุว่าค่าเก่ายังอยู่ใน State Bag ระหว่าง Callback ทำงาน

reserved คืออะไร

ปัจจุบันไม่ได้ใช้งาน

replicated คืออะไร

บอกว่า State Set นั้นถูกกำหนดให้ Replicate หรือไม่

replicated=true แปลว่ามาจาก Serverไหม

ไม่ควรตีความแบบนั้น มันไม่ได้เป็น Security Proof

Handler ยกเลิก State Change ได้ไหม

ไม่ได้ในปัจจุบัน เอกสารระบุว่า Change Handler ไม่สามารถ Reject Change ได้

วิธีหา Entity จาก bagName

ใช้

GetEntityFromStateBagName(
    bagName
)

วิธีหา Player จาก bagName

ใช้

GetPlayerFromStateBagName(
    bagName
)

GetEntityFromStateBagName คืน 0 ได้ไหม

ได้ หากไม่มี Entity ที่ Valid ใน Context ปัจจุบัน

Handler ใช้ Wait ได้ไหม

ได้ใน Lua Coroutine Context ตามตัวอย่าง Cfx.re แต่ต้องคำนึงว่า Entity/State/Ownership อาจเปลี่ยนระหว่างรอ

Handler ใช้แทน CreateThread ได้ไหม

ใช้แทน Polling ได้ดีเมื่อ Requirement คือทำงานตอน State เปลี่ยน แต่ไม่ได้แทน Thread ที่ต้อง Render หรือทำงานทุก Frame

Handler ใช้กับ Vehicle Fuel ได้ไหม

ได้

Handler ใช้กับ Player Duty ได้ไหม

ได้

Handler ใช้กับ Handcuff ได้ไหม

ได้ ถ้า Architecture ของ State/Authority ถูกต้อง

Handler ปลอดภัยจาก Client Cheat ไหม

ไม่โดยอัตโนมัติ Security ขึ้นกับว่าใครสามารถเขียน State และ Server ตรวจ Authority อย่างไร

Handler ถูกเรียกหลายครั้งได้ไหม

ได้เมื่อ State เปลี่ยนหลายครั้ง

AddStateBagChangeHandler Return อะไร

Return Handler Cookie แบบ Integer

ลบ Handler ได้ไหม

มี RemoveStateBagChangeHandler(cookie) แต่เอกสาร Cfx.re ปัจจุบันระบุ Native นี้เป็น Experimental

สรุป FiveM State Bag Change Handler คืออะไร

FiveM State Bag Change Handler คือระบบ Reactive สำหรับทำงานทันทีเมื่อ State Bag เปลี่ยน โดยใช้ AddStateBagChangeHandler()

โครงสร้างพื้นฐานคือ

AddStateBagChangeHandler(
    'state:key',
    nil,
    function(
        bagName,
        key,
        value,
        reserved,
        replicated
    )

        -- react to change

    end
)

แนวคิดสำคัญคือ

Polling
=
ถามซ้ำว่า State เปลี่ยนหรือยัง

แต่

State Bag Change Handler
=
State เปลี่ยนแล้ว FiveM เรียกเรา

จึงเหมาะกับข้อมูลอย่าง

Vehicle Lock
Fuel
Player Duty
Cuffed State
Entity Initialization
Mission State
HUD State

โดยเฉพาะ State ที่เปลี่ยนไม่บ่อยและไม่จำเป็นต้องตรวจทุก Frame

แต่ต้องจำรายละเอียดสำคัญหลายข้อ ได้แก่

bagName
→ ใช้ Resolve Entity/Player

value
→ ค่าใหม่

State Bag ระหว่าง Callback
→ ยังสามารถมีค่าเก่า

entity == 0
→ ต้อง Handle

replicated
→ ไม่ใช่ Security Proof

Handler
→ Reject State Change ไม่ได้

Ownership
→ สามารถเปลี่ยนระหว่าง Async Logic

สำหรับผู้ที่เรียน FiveM Developer กับ comsiam สิ่งสำคัญคืออย่าเปลี่ยนทุก CreateThread() ให้เป็น State Handler แบบไม่คิด แต่ให้เลือกตามลักษณะของข้อมูล

ถ้าต้อง Draw Marker ทุก Frame ยังต้องใช้ Thread แต่ถ้าต้องรู้เพียงว่า locked เปลี่ยนจาก false เป็น true เมื่อไร AddStateBagChangeHandler() มักเหมาะกว่าการ Poll ทุก 500 ms

หลักจาก comsiam ที่ควรจำคือ State Bag ใช้เก็บสถานะ ส่วน State Bag Change Handler ใช้ตอบสนองเมื่อสถานะนั้นเปลี่ยน เมื่อใช้สองอย่างนี้คู่กันอย่างถูกต้อง Resource จะมีโครงสร้างแบบ Event-driven มากขึ้นและลดงานซ้ำที่ไม่จำเป็นได้มาก

หัวข้อถัดไปคือ FiveM Routing Bucket คืออะไร ซึ่งจะเข้าสู่ระบบ Instance/Dimension ของ OneSync ว่า Player และ Entity ถูกแยกโลกกันอย่างไร และทำไม Routing Bucket จึงสำคัญกับ Character Selection, Mission Instance, Lobby และ Interior System

Comments

Popular posts from this blog

FiveM ยังน่าเล่นไหม? Enhanced เปลี่ยน FiveM แค่ไหน

FiveM คืออะไร เล่นอย่างไร สำหรับมือใหม่ เริ่มต้นตั้งแต่ศูนย์

วิธีตั้ง Admin Permission ด้วย add_ace และ add_principal FiveM แบบละเอียด