Recursos TypeScript do FiveM: fxmanifest, tipos @citizenfx e esbuild

Escreva recursos FiveM em TypeScript ou JavaScript: fxmanifest com arquivos dist, tipos @citizenfx, um bundle esbuild, eventos, setTick e Wait, e chamando exports Lua.

Você quer tipos, async/await e pacotes npm em seu script FiveM, mas seu recurso é apenas arquivos .lua. FiveM executa JavaScript em seu próprio tempo de execução, portanto TypeScript funciona desde que seja compilado para JavaScript primeiro. Este guia fornece o layout funcionando: manifest, tipos, bundler, eventos e ticks, e como misturá-lo com Lua.

Layout do projeto

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

Você edita src/, constrói em dist/, e o manifest só conhece sobre dist/.

O fxmanifest

lua
fx_version 'cerulean'
game 'gta5'

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

Ambos são arquivos JavaScript simples. Se seu recurso precisar de uma versão particular do runtime JavaScript ou funcionalidades Node no servidor, verifique a documentação atual do Cfx.re para as opções do manifest, pois os tempos de execução suportados mudaram ao longo do tempo.

Dica: se você deixar de fora a construção, FiveM diz que não consegue encontrar dist/client.js. Sempre execute a construção antes de ensure ou restart.

Instale tipos e um bundler

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

@citizenfx/client e @citizenfx/server são os pacotes de tipo para os natives e funções Cfx.re em cada lado. Um tsconfig.json que os usa:

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

TypeScript apenas verifica tipos aqui (noEmit). esbuild faz o bundling real:

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 significa pacotes npm que você importa são copiados para o único arquivo de saída, portanto o servidor não precisa de node_modules em tempo de execução. Verifique a documentação Cfx.re para a --platform e --target corretas para o tempo de execução que seus artefatos usam.

Eventos

JavaScript tem os mesmos quatro blocos de construção que Lua, com outros nomes:

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)
})

No servidor, o jogador que disparou o evento está no source global, como em Lua. Leia-o em uma constante no início do handler: após um await, o global pode apontar para outro jogador. As mesmas regras de segurança de eventos do servidor de segurança se aplicam, já que o jogador pode disparar seu onNet de fora. Mais sobre o modelo de evento em eventos de cliente e servidor.

Ticks e espera

Não há loop de quadro por padrão. setTick executa uma função a cada quadro, e Wait do Lua se torna uma promise em torno de 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
})

Sem um atraso, o handler é executado a cada quadro, o mesmo problema que um loop Lua sem Wait(0): veja Wait e threads explicadas. Use um atraso mais longo quando nada precisar ser verificado a cada quadro.

Para um atraso único, pule setTick e apenas await Delay(1000) dentro de uma função async.

Misturando com exports Lua

Exports funcionam entre linguagens. Registre um em JavaScript:

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

E chame-o de um recurso Lua:

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

A outra direção, chamando um export Lua de TypeScript, passa pelo objeto exports:

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

Declare a forma você mesmo se os tipos não conhecerem o outro recurso, por exemplo com declare const exports: Record<string, any>. Veja exports em Lua para como eles se comportam quando o recurso de destino reinicia.

Construa antes de começar

Execute o observador enquanto você trabalha, e reinicie o recurso no console após cada construção:

text
npm run watch
restart my_resource

Se um reinício não fizer nada novo, verifique se o arquivo de saída mudou, se o manifest aponta para ele, e se o recurso não é servido a partir de uma cópia antiga em cache/.

Checklist

Sintoma Correção
FiveM não consegue encontrar dist/client.js Execute a construção; o manifest carrega a saída, não src/
Natives aparecem como desconhecidos no editor Instale @citizenfx/client e @citizenfx/server
import falha em tempo de execução Bundle com esbuild para que as importações sejam incorporadas
Handler usa o jogador errado Leia source em uma constante antes de qualquer await
Alto resmon de um tick Adicione await Delay(ms) ao handler setTick
Lua não consegue ver sua função Registre-a com exports('name', fn) e chame-a pelo nome do recurso

Respostas rápidas

Posso usar TypeScript em um recurso FiveM diretamente?

Não. FiveM executa JavaScript. Você escreve TypeScript, o agrupa em JavaScript simples com uma ferramenta como esbuild, e aponta client_script e server_script para os arquivos de saída.

Um recurso TypeScript pode chamar exports Lua?

Sim. Exports são compartilhados entre linguagens: chame exports['my_resource'].functionName() do JavaScript, e Lua pode chamar exports que você registra com exports('name', fn) em JavaScript.

O que substitui Wait() em JavaScript?

Use uma promise em torno de setTimeout dentro de uma função async ou um handler setTick. O loop de ticking em si é setTick, e cada tick deve esperar um atraso para que não seja executado a cada quadro.

Scripts sem esse problema

Quest CreatorUm editor visual de missões e diálogos com NPCs, montado nó por nó dentro do jogo.Ver script →Item Creator V2Crie itens usáveis com animações, props, efeitos e muito mais — sem escrever código.Ver script →Shop CreatorMonte uma loja em menos de um minuto — donos, funcionários, cofres e assaltos inclusos.Ver script →

Continue lendo