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.
Макет проекта
my_resource/
fxmanifest.lua
package.json
tsconfig.json
src/
client.ts
server.ts
dist/ (build output, loaded by FiveM)Вы редактируете src/, собираете в dist/, и манифест знает только о dist/.
Fxmanifest
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
npm init -y
npm install --save-dev typescript esbuild @citizenfx/client @citizenfx/server@citizenfx/client и @citizenfx/server — это пакеты типов для нативов и функций Cfx.re на каждой стороне. tsconfig.json, который их использует:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}TypeScript только проверяет типы здесь (noEmit). esbuild выполняет фактическую сборку:
{
"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 |
// src/server.ts
onNet('my_resource:requestData', () => {
const src = (global as any).source as number
emitNet('my_resource:data', src, { hello: 'world' })
})// 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:
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:
exports('getGreeting', (name: string) => `Hello ${name}`)И вызовите его из ресурса Lua:
local text = exports['my_resource']:getGreeting('Mic')Другое направление, вызов Lua export из TypeScript, проходит через объект exports:
const ok = exports['ox_inventory'].GetItemCount(source, 'bread')Объявите форму самостоятельно, если типы не знают другой ресурс, например с помощью declare const exports: Record<string, any>. См. exports в Lua для того, как они работают, когда целевой ресурс перезапускается.
Соберите перед началом
Запустите watcher, пока вы работаете, и перезагрузите ресурс в консоли после каждой сборки:
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Магазин меньше чем за минуту — владельцы, сотрудники, сейфы и ограбления в комплекте.Смотреть скрипт →