lua-best-practices について
FiveM リソース開発者向けの Lua 実践ガイドです。Overextended のリソースと ox_lib を基準に、コードやデータの読み込み、マニフェスト、設定、実行時 API、コード配置を説明します。
主な機能
- 01require と lib.load によるコード・データ読み込み
- 02リソースのマニフェストとエントリースクリプトの整理
- 03convars による設定管理
- 04ox_lib のキャッシュと実行時 API の利用
- 05CfxLua の構文と注釈の適用
活用シーン
- 01FiveM リソースの構成設計
- 02Lua モジュールとデータ読み込みの実装
- 03ox_lib 実行時機能の設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ overextended/ skills --skill lua-best-practicesインストール後、Claude Code で/lua-best-practicesを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlPATTERNS.md
詳細
Lua のベストプラクティス
Overextended が ox_inventory、ox_target、ox_fuel を作る方法に倣って、リソースの Lua を記述します。サイドごとにエントリースクリプトを 1 つだけ用意し、それ以外のファイルはすべて require で読み込むモジュールにします。データは lib.load で読み込み、設定は convar から読み取ります。また、独自のループを手書きするのではなく、ox_lib の cache と lib.* モジュールを使います。ox_lib 3.40.3 に基づいて確認しています。サーバーの ox_lib バージョンは、現在のディレクトリから上にたどって .ox/config.md の ox resources の行で確認してください。バージョンが古い場合は、以下の内容を該当バージョンのソースで確認してください。
@ox_lib/init.lua を含めると、グローバルの require が ox_lib 独自のローダーに置き換わります。このローダーは呼び出し元のファイルではなく、リソースのルートからパスを解決し、他のリソースのファイルも読み込めます。以下はすべて、このローダーを前提としています。
コードとデータの読み込み
| 呼び出し | 用途 | 動作 |
|---|---|---|
require 'client.utils' | コードモジュール:関数や状態を定義してテーブルを return するファイル | リソースとサイドごとにファイルを 1 回実行し、その後の呼び出しにはキャッシュされた値を返します。そのため、すべてのモジュールが同じインスタンスを共有します。循環 require があるとエラーになります。何も返さないファイルは true を返します。 |
lib.load 'data.stations' | データファイル(テーブルを返す data/*.lua)と、副作用のために実行するファイル | 呼び出すたびにファイルを実行し、新しい値を返します。キャッシュはされません。第 2 引数として環境テーブルを省略可能で指定できます。 |
lib.loadJson 'data.prices' | JSON データファイル | data/prices.json を読み込み、デコードしたテーブルを返します。 |
require '@ox_core.lib.init' | 他のリソースの Lua ライブラリ | ファイルを自分のリソース内で実行するため、共有状態ではなく、自分用のコピーを取得します。他のリソースの現在の状態を読み取るには、そのリソースのエクスポートを使ってください。 |
3 つすべてに共通するパスのルール:
- ドットはディレクトリの区切りで、拡張子は指定しません:
require 'modules.inventory.server'はmodules/inventory/server.luaを読み込みます。ファイル名にドットは使えません。 require 'client'は、まずclient.lua、次にclient/init.luaを試します。@resource.pathは別のリソースから読み込みます。@resourceだけを指定すると、そのリソースのinit.luaを読み込みます。- クライアントで読み込むファイルは、マニフェストの
filesに記載する必要があります。サーバー側のファイルはディスクから読み込まれるため、記載は不要です。
使い分け:
- コードは
requireで読み込みます。 使用するファイルの先頭にlocal utils = require 'client.utils'を置き、すべてのモジュールの末尾でテーブルをreturnします。 - データは
lib.loadで読み込みます。 データを使用する箇所で一度呼び出します(for id, data in pairs(lib.load('data.crafting') or {}) do)。キャッシュされないため、ループ内やイベントハンドラー内では決して呼び出さないでください。 - 複数のモジュールで共有する読み取り専用の設定は、モジュールにできます。
config.luaでテーブルを返し、各ファイルでlocal config = require 'config'とします。 - ファイルはマニフェストスクリプトかモジュールのどちらか一方です。 ファイルを
client_scriptsに記載したうえでrequireもすると、2 回実行されます。 - オプションの部分は必要になったときに読み込みます:
if config.ox_target then return require 'client.target' endのようにするか、対応するフレームワークを検出した分岐内でのみフレームワークブリッジを読み込みます。 LoadResourceFileとloadを組み合わせた独自の処理は書かないでください。ox_inventory 独自のdata()ローダーは非推奨であり、lib.loadが推奨されています。
マニフェスト
- すべての ox リソースと同様に、
fx_version 'cerulean'、lua54 'yes'、use_experimental_fxv2_oal 'yes'を指定します。 shared_script '@ox_lib/init.lua'は、リソース独自のスクリプトより前に記述し、ox_libをdependenciesに含めます。- 各サイドにつきエントリースクリプトは1つです(
client/init.lua、server/init.lua、またはlib.contextに応じてrequire 'server'かrequire 'client'で終わる単一のinit.lua)。エントリースクリプトから、それ以外のすべてを読み込みます。 filesには、クライアントが読み込むすべてのモジュール、データファイル、ロケールを指定します('client/*.lua'、'data/*.lua'、'locales/*.json')。クライアントはfiles内のすべてをダウンロードするため、サーバー専用のコードは含めないでください。ox_libs { 'locale', 'table' }は、リソースの起動時にこれらのlibモジュールを読み込みます。ここにlocaleを指定するとロケール文字列も読み込まれます。これはlib.locale()を自分で呼び出すより ox_lib に適した方法です。それ以外のlib.*モジュールは初回使用時に読み込まれるため、決してrequireしないでください。
設定
- サーバー所有者が調整する設定は、
<resource>:<setting>という名前の convar です。使用するファイルの先頭で一度だけ読み込みます。例:GetConvarInt('ox_target:debug', 0)、GetConvarBool('inventory:target', false)、GetConvar('ox_target:defaultHotkey', 'LMENU')。構造化された設定はjson.decode(GetConvar(...))を通して取得します。所有者はserver.cfgで設定するため、アップデートによって変更が上書きされることはありません。 - 構造化データ(場所、アイテム、ショップ、レシピ)は
data/*.luaファイルにテーブルとして記述し、lib.loadで読み込みます。 - 小規模なリソースでは、テーブルを返す
config.luaをモジュールとして読み込めます。リソースがすでに採用している方法に従ってください。
スコープ
- すべての変数、関数、モジュールテーブルは
localにします。モジュールはlocal Utils = {}とし、関数はfunction Utils.raycast(...)のように定義して、末尾でreturn Utilsを記述します。 - グローバル変数は、そのサイドのすべてのファイルから読み取る状態を保持するために意図的に使います。
localの付け忘れであってはなりません。同じサイドにあるリソースのスクリプトとモジュールは、すべて同じグローバルスコープを共有します。 - ホットループ内では、ネイティブをローカルのアップバリューとしてキャッシュします。例:
local GetVehiclePedIsIn = GetVehiclePedIsIn
レイアウト
コードは上から下へ、短いステップに分けて記述し、ステップの間に空行を入れ、長い行はなるべく避けます。
- 1行につき1つのステートメントを記述します。
count = 0 return falseは2行に分けます。 - ステップの間には空行を入れます。ガード、検索、処理、応答の間などです。
- ガードを1行にするのは、短く、本文が何も返さない
returnまたはreturn falseだけの場合です。例:if not player or not player.shop then return false end。テーブルを返すガードや、それ以外の処理を行うガードは複数行にします。 - 1行に収まらない条件は、複数のガードに分けます。1つの
ifを複数行に折り返すのは避けてください。 - 呼び出しやテーブルコンストラクターが約120文字を超える場合は、引数またはフィールドを1行ずつ記述します。それより短いものは1行にまとめます。
- インデントには
.editorconfigまたはファイルで使われている形式を使用します。
実行時の ox_lib
cacheにはクライアント上のプレイヤーの状態が保持され、100 ms ごとに更新されます。cache.ped、cache.vehicle(徒歩の場合はfalse)、cache.seat、cache.weaponに加え、cache.playerIdとcache.serverIdが含まれます。両側にはcache.resourceとcache.gameもあります。PlayerPedId()ではなく、cache.pedを参照してください。lib.onCache('vehicle', function(value, oldValue) end)は、キャッシュされた値が変わると実行されます。変化をポーリングするループの代わりに使ってください。- スレッド:
CreateThreadとWaitを使います。繰り返し処理には ox_lib のSetInterval(fn, ms)とClearInterval(id)を使います。「場所の近くで何かを行う」場合は、距離を確認するループではなく、lib.points.new({ coords = ..., distance = ..., onEnter = ..., onExit = ..., nearby = ... })を使ってください。値を待つにはlib.waitFor(fn, errMessage, timeout)を使います。timeoutms(デフォルトは 1000)を過ぎるとエラーになります。 - コールバック: サーバーでは
lib.callback.register(name, function(source, ...) end)を登録し、クライアントではlib.callback.await(name, delay, ...)を呼び出します。delayはクライアント側のレート制限(ms)またはfalseです。スロットリングされた呼び出しは何も返しません。クライアントはすべての引数を選べるため、サーバーのコールバックでは処理を実行する前に各引数(型、範囲、所有権、距離)を確認してください。 - ロケール:
locales/en.jsonはフォールバックであり、ox:localeconvar で設定された言語の下にマージされます。locale('key', ...)はstring.formatで書式を整え、該当するキーがない場合はキー自体を返します。 - ログ:
lib.print.error、warn、info、verbose、debugを使います。管理者はox:printlevelconvar、または単一のリソースに対してはox:printlevel:<resource>で出力を絞り込めます。 - 依存関係: エントリースクリプトの先頭で
if not lib.checkDependency('ox_lib', '3.30.0', true) then return endを使い、最低バージョンを確認してください。任意のリソースはGetResourceState('ox_target') == 'started'でガードしてください。
CfxLua
FiveM の Lua 5.4 ランタイムには、ox リソース全体で使われる構文が追加されています。次のように記述してください。
- 定数名にはバッククォートのハッシュを使います。
`WEAPON_UNARMED`はコンパイル時にハッシュ化されます。実行時に組み立てる名前にはjoaat(name)を使ってください。 - 安全なナビゲーション:
data.server and data.server.exportの代わりにdata.server?.exportを使います。 - 複合代入:
count += 1。 - 事前確保には
table.create(arraySize, hashSize)、その場でクリアするにはtable.wipe(t)を使います。
アノテーション
モジュール関数には LuaLS コメント(---@param、---@return、---@class)でアノテーションを付け、値の型を推論できない場合は --[[@as OxClient]] で型を絞り込んでください。ox_lib には独自のアノテーションが含まれているため、エディターで lib.* の呼び出しの型がチェックされます。
例
完全なリソース構成と各セクションの例については、PATTERNS.md を参照してください。