mcpField
L
lua-best-practices

lua-best-practices

github.com/overextended开发209 次安装2 周前更新
代码仓库

关于 lua-best-practices

面向 FiveM 资源开发者的 Lua 实践指南,以 Overextended 资源和 ox_lib 为参照,涵盖代码加载、清单、配置、运行时及代码布局。

主要功能

  1. 01使用 require 与 lib.load 加载代码和数据
  2. 02组织资源清单与入口脚本
  3. 03通过 convars 管理设置
  4. 04应用 ox_lib 缓存与运行时 API
  5. 05遵循 CfxLua 语法与注解

适用场景

  1. 01规划 FiveM 资源文件结构
  2. 02实现 Lua 模块与数据加载
  3. 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:locale convar 指定的语言合并。locale('key', ...) 使用 string.format 格式化;如果键不存在,则返回键本身。
  • 日志:lib.print.error、warn、info、verbose、debug。资源所有者可通过 ox:printlevel convar 筛选日志级别,也可通过 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,了解完整的资源布局以及各部分的示例。

评论

支持 Markdown