关于 lua-best-practices
面向 FiveM 资源开发者的 Lua 实践指南,以 Overextended 资源和 ox_lib 为参照,涵盖代码加载、清单、配置、运行时及代码布局。
主要功能
- 01使用 require 与 lib.load 加载代码和数据
- 02组织资源清单与入口脚本
- 03通过 convars 管理设置
- 04应用 ox_lib 缓存与运行时 API
- 05遵循 CfxLua 语法与注解
适用场景
- 01规划 FiveM 资源文件结构
- 02实现 Lua 模块与数据加载
- 03配置 ox_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:每一端各有一个入口脚本,其他文件都作为模块,通过 require 加载;通过 lib.load 加载数据;从 convar 读取设置;使用 ox_lib 的 cache 和 lib.* 模块,而不是手写循环。已根据 ox_lib 3.40.3 检查。请阅读 .ox/config.md 中的 ox resources 行(从当前目录向上查找),以确认服务器的 ox_lib 版本;如果版本较旧,请根据该版本的源码检查以下内容。
包含 @ox_lib/init.lua 会将全局 require 替换为 ox_lib 自带的加载器。它以资源根目录而非调用文件所在目录为基准解析路径,并且可以加载其他资源中的文件。以下内容均以此为前提。
加载代码和数据
| 调用 | 用途 | 行为 |
|---|---|---|
require 'client.utils' | 代码模块:定义函数或状态并 return 一个表的文件 | 每个资源、每一端只运行一次,之后的每次调用都会返回缓存值,因此所有模块共享同一个实例。循环 require 会报错。文件不返回任何内容时会返回 true。 |
lib.load 'data.stations' | 数据文件(返回表的 data/*.lua 文件),以及用于执行副作用的文件 | 每次调用都会运行文件并返回新值;不会缓存。可选的环境表作为第二个参数传入。 |
lib.loadJson 'data.prices' | JSON 数据文件 | 读取 data/prices.json 并返回解码后的表。 |
require '@ox_core.lib.init' | 另一个资源的 Lua 库 | 在你的资源中运行该文件:你会获得自己的副本,而不是共享状态。请通过另一个资源的导出接口读取其实时状态。 |
以下路径规则适用于以上三种调用:
- 点号用于分隔目录,且不写扩展名:
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 加载,会导致它运行两次。 - 按需延迟加载可选部分:
if config.ox_target then return require 'client.target' end,也可以仅在检测到对应框架的分支中加载框架桥接模块。 - 不要手动编写
LoadResourceFile和load的组合。ox_inventory 自带的data()加载器已弃用,应改用lib.load。
清单
- 使用
fx_version 'cerulean'、lua54 'yes'和use_experimental_fxv2_oal 'yes',与每个 ox 资源保持一致。 shared_script '@ox_lib/init.lua'必须置于资源自有脚本之前,并将ox_lib列入dependencies。- 每端使用一个入口脚本(
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也会加载本地化字符串,这是 ox_lib 优先采用的方式,而不是自行调用lib.locale()。其他任何lib.*模块都会在首次使用时加载,因此绝不要对其执行require。
配置
- 由服务器所有者调整的设置使用名为
<resource>:<setting>的 convar,并在使用它们的文件顶部读取一次:GetConvarInt('ox_target:debug', 0)、GetConvarBool('inventory:target', false)、GetConvar('ox_target:defaultHotkey', 'LMENU')。结构化设置通过json.decode(GetConvar(...))读取。所有者在server.cfg中设置这些值,因此更新不会覆盖他们的更改。 - 结构化数据(位置、物品、商店、配方)存放在返回表并通过
lib.load加载的data/*.lua文件中。 - 小型资源可以使用返回表、并作为模块引入的
config.lua。遵循资源现有的做法即可。
作用域
- 每个变量、函数和模块表都使用
local。模块的写法是local Utils = {},其函数写作function Utils.raycast(...),并在末尾return Utils。 - 全局变量应是刻意选择,用于存放该端每个文件都会读取的状态,而不是遗漏了
local。同一端上,一个资源的所有脚本和模块共享同一个全局作用域。 - 在热循环中,将原生函数缓存为局部上值:
local GetVehiclePedIsIn = GetVehiclePedIsIn。
排版
代码从上到下分成简短步骤书写,步骤之间留空行,并尽量避免长行。
- 每行一条语句。
count = 0 return false应拆成两行。 - 步骤之间留一个空行:条件检查、查找、执行操作、返回结果。
- 只有当条件检查较短且代码块仅包含裸
return或return false时,才将其写在一行:if not player or not player.shop then return false end。如果返回表或执行更多操作,则应分行书写。 - 条件太长而无法写在一行时,应拆成多个条件检查,绝不要把一个
if跨行书写。 - 调用或表构造器超过约 120 个字符时,每行写一个参数或字段。更短的则保持在一行。
- 按照
.editorconfig或文件现有的缩进方式缩进。
ox_lib 运行时
cache保存客户端玩家的状态,每 100 ms 刷新一次:cache.ped、cache.vehicle(步行时为false)、cache.seat、cache.weapon,以及cache.playerId和cache.serverId。客户端和服务端两侧都有cache.resource和cache.game。读取cache.ped,而不要读取PlayerPedId()。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);超过timeout毫秒后会报错(默认 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?.export替代data.server and data.server.export。 - 复合赋值:
count += 1。 - 使用
table.create(arraySize, hashSize)预分配空间,使用table.wipe(t)原地清空。
注解
使用 LuaLS 注释(---@param、---@return、---@class)为模块函数添加注解;当无法推断值的类型时,使用 --[[@as OxClient]] 缩窄类型。ox_lib 自带注解,因此编辑器可以对 lib.* 调用进行类型检查。
示例
阅读 PATTERNS.md,了解完整的资源布局以及各部分的示例。