FiveM TypeScript ресурсы: fxmanifest, @citizenfx types и esbuild

Пишите FiveM ресурсы на TypeScript или JavaScript: fxmanifest с dist файлами, @citizenfx types, esbuild bundle, события, setTick и Wait, и вызов Lua exports.

Вы хотите типы, async/await и пакеты npm в вашем скрипте FiveM, но ваш ресурс просто .lua файлы. FiveM запускает JavaScript в своём собственном runtime, поэтому TypeScript работает при условии, что он скомпилирован в JavaScript в первую очередь. Это руководство даёт правильный макет: манифест, типы, bundler, события и тики, и как смешивать его с Lua.

Макет проекта

text
my_resource/
  fxmanifest.lua
  package.json
  tsconfig.json
  src/
    client.ts
    server.ts
  dist/            (build output, loaded by FiveM)

Вы редактируете src/, собираете в dist/, и манифест знает только о dist/.

Fxmanifest

lua
fx_version 'cerulean'
game 'gta5'

client_script 'dist/client.js'
server_script 'dist/server.js'

Оба — это простые файлы JavaScript. Если вашему ресурсу требуется определённая версия JavaScript runtime или функции Node на сервере, проверьте текущую документацию Cfx.re по параметрам манифеста, так как поддерживаемые runtime изменялись со временем.

Совет: если вы оставите сборку, FiveM скажет, что не может найти dist/client.js. Всегда запускайте сборку перед ensure или restart.

Установите типы и bundler

bash
npm init -y
npm install --save-dev typescript esbuild @citizenfx/client @citizenfx/server

@citizenfx/client и @citizenfx/server — это пакеты типов для нативов и функций Cfx.re на каждой стороне. tsconfig.json, который их использует:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

TypeScript только проверяет типы здесь (noEmit). esbuild выполняет фактическую сборку:

json
{
  "scripts": {
    "build": "esbuild src/client.ts --bundle --outfile=dist/client.js --target=es2020 && esbuild src/server.ts --bundle --outfile=dist/server.js --target=es2020 --platform=node",
    "watch": "npm run build -- --watch",
    "typecheck": "tsc"
  }
}

Bundling означает, что пакеты npm, которые вы импортируете, копируются в один выходной файл, поэтому серверу не требуется node_modules во время выполнения. Проверьте документацию Cfx.re для правильного --platform и --target для runtime, который используют ваши артефакты.

События

JavaScript имеет те же четыре строительных блока, что и Lua, с другими названиями:

Lua JavaScript
AddEventHandler on
RegisterNetEvent + handler onNet
TriggerEvent emit
TriggerServerEvent / TriggerClientEvent emitNet
ts
// src/server.ts
onNet('my_resource:requestData', () => {
  const src = (global as any).source as number
  emitNet('my_resource:data', src, { hello: 'world' })
})
ts
// src/client.ts
on('onClientResourceStart', (resourceName: string) => {
  if (GetCurrentResourceName() !== resourceName) return
  emitNet('my_resource:requestData')
})

onNet('my_resource:data', (data: { hello: string }) => {
  console.log(data.hello)
})

На сервере игрок, который вызвал событие, находится в глобальном source, как в Lua. Прочитайте его в константу в начале обработчика: после await глобальный может указывать на другого игрока. Те же правила безопасности из защиты событий сервера применяются, так как игрок может запустить вашу onNet снаружи. Подробнее о модели событий в события клиента и сервера.

Тики и ожидание

По умолчанию нет цикла кадров. setTick запускает функцию каждый кадр, и Wait из Lua становится промисом вокруг setTimeout:

ts
const Delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))

setTick(async () => {
  const ped = PlayerPedId()
  const [x, y, z] = GetEntityCoords(ped, true)
  // ... check something about the position
  await Delay(500)   // without this, it runs every frame
})

Без задержки обработчик выполняется каждый кадр, та же проблема, что и цикл Lua без Wait(0): см. Wait и потоки объяснены. Используйте более длительную задержку, если ничего не нужно проверять каждый кадр.

Для одноразовой задержки пропустите setTick и просто await Delay(1000) внутри асинхронной функции.

Смешивание с Lua exports

Exports работают на разных языках. Зарегистрируйте один в JavaScript:

ts
exports('getGreeting', (name: string) => `Hello ${name}`)

И вызовите его из ресурса Lua:

lua
local text = exports['my_resource']:getGreeting('Mic')

Другое направление, вызов Lua export из TypeScript, проходит через объект exports:

ts
const ok = exports['ox_inventory'].GetItemCount(source, 'bread')

Объявите форму самостоятельно, если типы не знают другой ресурс, например с помощью declare const exports: Record<string, any>. См. exports в Lua для того, как они работают, когда целевой ресурс перезапускается.

Соберите перед началом

Запустите watcher, пока вы работаете, и перезагрузите ресурс в консоли после каждой сборки:

text
npm run watch
restart my_resource

Если перезагрузка ничего не делает, проверьте, что выходной файл изменился, что манифест указывает на него, и что ресурс не обслуживается из устаревшей копии в cache/.

Чеклист

Симптом Исправление
FiveM не может найти dist/client.js Запустите сборку; манифест загружает выходные данные, а не src/
Нативы показаны как неизвестные в редакторе Установите @citizenfx/client и @citizenfx/server
import не работает во время выполнения Соберите с помощью esbuild, чтобы импорты были встроены
Обработчик использует неправильного игрока Прочитайте source в константу перед любым await
Высокий resmon из тика Добавьте await Delay(ms) к обработчику setTick
Lua не видит вашу функцию Зарегистрируйте её с помощью exports('name', fn) и вызовите по имени ресурса

Короткие ответы

Могу ли я использовать TypeScript в ресурсе FiveM напрямую?

Нет. FiveM запускает JavaScript. Вы пишете TypeScript, собираете его в обычный JavaScript с помощью инструмента, такого как esbuild, и указываете client_script и server_script на выходные файлы.

Может ли ресурс TypeScript вызывать Lua exports?

Да. Exports используются совместно между языками: вызывайте exports['my_resource'].functionName() из JavaScript, и Lua может вызывать exports, которые вы регистрируете с помощью exports('name', fn) в JavaScript.

Что заменяет Wait() в JavaScript?

Используйте промис вокруг setTimeout внутри асинхронной функции или обработчика setTick. Сам тикующий цикл — это setTick, и каждый тик должен ожидать задержку, чтобы он не выполнялся каждый кадр.

Скрипты без этой проблемы

Quest CreatorВизуальный редактор квестов и диалогов с NPC — узел за узлом прямо в игре.Смотреть скрипт →Item Creator V2Создавайте используемые предметы с анимациями, пропами, эффектами и не только — без кода.Смотреть скрипт →Shop CreatorМагазин меньше чем за минуту — владельцы, сотрудники, сейфы и ограбления в комплекте.Смотреть скрипт →

Читайте также