FiveM scriptの基礎と開発メモ

# FiveM Scriptの基礎と開発について
今までFiveMのScriptを開発してきたノウハウを備忘録として残しています。
一部はまだ動作未検証の部分があったり、環境依存のものもあります。筆者の環境は、QBCoreやox_lib、ox_target、ox_inventoryを使用している環境です。
ESXには対応していないのでご注意ください。

## FiveM scriptの構成と役割
・fxmanifest.lua ・・・スクリプト構成を記載（記載のないものは読み込まれない）
・client.lua・・・クライアント側で動作するスクリプト（モーションなど）
・server.lua・・・サーバ側で動作するスクリプト（アイテム、お金の付与など）
・config.lua(shard.lua)・・・スクリプトを動作させるための設定ファイル（座標など）


## ざっくりイメージ図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/afbc3883-7865-3bb7-1e2c-372f073838c4.png)

### fxmanifest.luaについて
スクリプトの構成を記載しており、記載のないものは読み込まれない。

```lua: fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

author 'KumaRider' --作成者名
description 'Gacha System with Sound for QBCore' --スクリプトの説明文
version '1.1.0' --スクリプトのバージョン

-- サーバースクリプト
server_scripts {
    'server/server.lua',
    'shared/shared.lua',  --server.luaでshard.luaを使用したいため記載
}

-- クライアントスクリプト
client_scripts {
    'client/client.lua',
}
--ここまでは基本的なスクリプトの構成

--ここから下はスクリプトによって必要なものを記載する。
-- QBCoreと依存関係があることを宣言
dependencies {
    'qb-core',
}

-- NUIファイル
ui_page 'html/index.html'

-- 必要なファイル
files {
    'html/index.html',
    'html/script.js',
    'html/sounds/*.ogg',
}
```

## クライアントサイドからサーバサイドの呼び出し
```lua: client.lua
TriggerServerEvent('gacha:roll', ticketType)
```
```lua: server.lua
RegisterNetEvent('gacha:roll')
AddEventHandler('gacha:roll', function(ticketType)
    処理内容
end)
```
TriggerServerEventでサーバに対してイベントを送信（発火）する。
RegisterNetEventで発火したイベントをフックする。
AddEventHandlerで実際にどう処理をするのか記載する。

## サーバサイドからクライアントサイドの呼び出し
```lua: server.lua
TriggerClientEvent('gacha:openGacha', source, ticketType)
```
```lua: client.lua
RegisterNetEvent('gacha:openGacha')
AddEventHandler('gacha:openGacha', function(ticketType)
    処理内容
end)
```
TriggerClientEcentでクライアントに対してイベントを送信（発火）する。
RegisterNetEventで発火したイベントをフックする。
AddEventHandlerで実際にどう処理をするのか記載する。

## 通知メッセージ（qbcore、ox_lib）
処理としては、サーバからqbcore,ox_libのイベントをクライアントに発火させる。
```lua: server.lua(qbcore)
TriggerClientEvent('QBCore:Notify', src, 'はずれ', 'success')
```
[src]がプレイヤー、[はずれ]がメッセージ、[success]が情報レベルでの表示

```lua: server.lua(ox_lib)
TriggerClientEvent('ox_lib:notify', src, 'You have received', 'success')
```
[src]がプレイヤー、[You hab received]がメッセージ、[success]が情報レベルでの表示

## お金の取引（cash、bank）
処理は、サーバ側での処理で実施すること。（不正防止のため）
```lua: server.lua
--ユーザ指定
local Player = QBCore.Functions.GetPlayer(src)

--銀行口座での取引（受取）
Player.Functions.AddMoney('bank', 1000, 'Transaction_Log')
--銀行口座での取引（支払）
Player.Functions.RemoveMoney('bank', 1000, 'Transaction_Log')
```
```lua: server.lua
--ユーザ指定
local Player = QBCore.Functions.GetPlayer(src)

--現金での取引（受取）
Player.Functions.AddMoney('cash', 1000, 'Transaction_Log')
--現金での取引（支払）
Player.Functions.RemoveMoney('cash', 1000, 'Transaction_Log')
```
Transaction_Logは任意の文字列で、なんの取引内容かを特定するためのテキストを入力する。

## アイテムの取引
アイテムの取引処理もサーバ側で実施すること。（不正防止のため）
cashはアイテムとしてもあるため、以下でのコードでも付与可能。
```lua: server.lua
--ユーザ指定
local Player = QBCore.Functions.GetPlayer(src)

--アイテムの付与
Player.Functions.AddItem(item_name, 10)
--アイテム付与の通知
TriggerClientEvent('inventory:client:ItemBox', src, item_name, "add")

--アイテムの削除
Player.Functions.RemoveItem(item_name, 10)
--アイテム削除の通知（未検証）
TriggerClientEvent('inventory:client:ItemBox', src, item_name, "remove")

--cashの付与
Player.Functions.AddItem(cash, 10)
--cashの削除
Player.Functions.RemoveItem(cash, 10)
```
## アイテム使用のフック

```lua: server.lua
QBCore.Functions.CreateUseableItem('example_item', function(source, item)
    処理内容
end)
```
※検証している中でox_inventoryからアイテム使用を以下で検知しようとしたが、うまく動作しなかったため、基本的には、QBCoreでフックするようにしている。
(2024.10.15追記)
アイテムの使用をフックするのではなく、アイテムを使用できるように登録するニュアンスのほうが正しい。


```lua: server.lua(ox_inbentory)
--うまくフックできなかったコード
RegisterNetEvent('ox_inventory:useItem', function(data)
```

## 座標と向きと範囲
vector3やvector4で確認する。（vector4は向きも込）
config.luaで設定するときは範囲も含めて以下のように記述することが多い。
```lua: config.lua
coords = vector3(31.84, -170.77, 54.42), --３つ目は高さ
heading = 162.9, --向き
radius = 5.0 --範囲
```
:::note warn
範囲は、球体の範囲になるので、広げると上下にも広がるので注意が必要
:::

## アニメーション関連
client.luaに定義する。amb@prop_human_bum_bin@baseがアニメーションの種類。

```lua: client.lua
TaskPlayAnim(playerPed, "amb@prop_human_bum_bin@base", "base", 8.0, -8.0, 3000, 16, 0, false, false, false)
```
アニメーションの種類は以下のサイトに纏められており、猫のアニメーションなども記載されている。

https://gtahash.ru/animations/?page=1

## オブジェクト（モデル）関連
オブジェクトは、クライアント側で生成する。
以下のコードは、自動販売機のモデルを生成するもので、config.luaで管理する構成でのコードである。

```lua: config.lua(shard.lua)
Config.VendingMachines = {
    {
        id = 'Vending_Machine_001',
        model = 'prop_vend_snak_01', -- 食品の自動販売機
        coords = vector3(-572.53, -1071.41, 21.33), -- 猫カフェ前座標
        heading = 0.0,
```

```lua: client.lua
Citizen.CreateThread(function()
    -- Configで定義された各自動販売機を配置
    for _, machineConfig in ipairs(Config.VendingMachines) do
        local vendingMachineModel = GetHashKey(machineConfig.model)
        local vendingMachineCoords = machineConfig.coords
        local vendingMachineHeading = machineConfig.heading

        -- モデルがロードされているか確認
        RequestModel(vendingMachineModel)
        while not HasModelLoaded(vendingMachineModel) do
            Citizen.Wait(500)
        end

        -- 自動販売機を生成し、配置
        local vendingMachineEntity = CreateObject(vendingMachineModel, vendingMachineCoords.x, vendingMachineCoords.y, vendingMachineCoords.z, true, false, true)
        SetEntityHeading(vendingMachineEntity, vendingMachineHeading) --向き
        FreezeEntityPosition(vendingMachineEntity, true) --動かないよう設定
        SetModelAsNoLongerNeeded(vendingMachineModel)
    end
end)
```
:::note info
解説メモ
	•	CreateObject は、指定したモデルを指定した座標に生成するための関数。
	•	第1引数：ハッシュ化された自動販売機のモデル。
	•	第2〜4引数：生成する場所の座標（X, Y, Z）。
	•	第5引数（true）：物理的な衝突を有効にするかどうか。
	•	第6引数（false）：生成されたオブジェクトが動的に生成されるかどうか（falseで静的なオブジェクト）。
	•	第7引数（true）：ネットワーク化するかどうか（true で他のプレイヤーとも同期されます）。
:::

以下の２つのサイトからObjectNameを検索して、組み込んでいます。


https://gta-objects.xyz/objects

https://gtahash.ru/

## ox_targetとメニュー
心の目で見た際に出てくる項目を出すためのコード
```lua: client.lua
-- ox_target Interaction (座標ベースのターゲットゾーンを追加)
for _, machineConfig in ipairs(Config.VendingMachines) do
    exports['ox_target']:addSphereZone({
        coords = machineConfig.coords,
        radius = 2.0,
        options = {
            --商品購入の項目（１つ目）
            {
                name = 'vendingMachineBuy',
                label = '商品を購入する',
                icon = 'fa-solid fa-shopping-cart',
                onSelect = function(data)
                    showProductMenu(machineConfig.coords)
                end
            },
            --売上金回収の項目（２つ目）
            {
                name = 'vendingMachineCollect',
                label = '売上金を回収する',
                icon = 'fa-solid fa-money-bill-wave',
                onSelect = function(data)
                    collectEarnings(machineConfig.coords)
                end
            }
        }
    })
end
```
以下の画像は３つだけど、「商品を購入する」と「売上金を回収する」を表示するためのコード。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/392c6ac0-7b30-9b5f-2fed-bdd1095124c9.png)

選択後の処理(onSelect)は、別の関数に飛ばして処理を記載している。
おそらく直接ここに記載しても動作すると思われる。

## HTMLとか、JavaScriptとか
NUIを利用して連携する事が可能。

ゲーム内に以下のようにHTMLベースのページを表示させ、JavaScriptと連携させて商品の購入等を実装。

■VipShopのスクリプト（自作）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3868301/74254ec4-0655-466e-1336-e15dd66cd296.png)


具体的なJavaScriptとの連携については、別の記事を作成予定。
（簡単な連携は以下の音声鳴動で記載）

## 音声鳴動
HTMLとJavaScriptを用いて音声を鳴動させる。
以下のサンプルは、server.luでclient.luaのイベントを発火させて、client.luaからHTML/Javascriptと連携して、音声を鳴動させている。

```lua: fxmanifest.lua
-- NUIファイル
ui_page 'html/index.html'

-- 必要なファイル
files {
    'html/index.html',
    'html/script.js',
    'html/sounds/*.ogg',
}
```
```lua: server.lua
TriggerClientEvent('gacha:playSound', src, 'get_s.ogg')
```
```lua: client.lua
RegisterNetEvent('gacha:playSound')
AddEventHandler('gacha:playSound', function(soundFile)
    SendNUIMessage({
        type = "playSound",
        sound = 'sounds/' .. soundFile,
        volume = 0.1 -- 必要に応じて調整
    })
end)
```
```html: index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Gacha Sound System</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <script src="script.js"></script>
  </body>
</html>
```
```javascript: script.js
window.addEventListener("message", function (event) {
  const data = event.data;

  if (data.type === "playSound") {
    const notificationSound = new Audio(data.sound);
    notificationSound.volume = data.volume || 1.0;
    notificationSound.play();
  }
});
```




## jobの取得とjobレベルの取得
```lua: client.lua
local playerJob = GetPlayerJob()
job_name = playerJob.name
job_lebel = playerJob.grade.level
job_onduty = playerJob.onduty --未検証（出勤状態かの取得）
```

## jobのon dutyの人数取得（出勤状態の取得）
jobでのon dutyを把握するためには、サーバで確認する必要があるので、サーバ側でコードを作成する。
```lua: server.lua
local src = source
local players = QBCore.Functions.GetPlayers(src) --サーバ上のすべてのプレイヤー取得
local uwuCount = 0

for i = 1, #players do　--#playersでサーバの人数分（要素数分）ループする。
    local player = QBCore.Functions.GetPlayer(players[i])
    if (player.PlayerData.job.name == "uwu" and player.PlayerData.job.onduty) then
        uwuCount = uwuCount + 1
    end
end
```

# その他
一旦はここまでで順次追記していこうと思います。
あと別記事で作成したスクリプト記事も書こうと思います。