Recursos TypeScript de FiveM: fxmanifest, tipos @citizenfx y esbuild

Escribe recursos de FiveM en TypeScript o JavaScript: fxmanifest con archivos dist, tipos @citizenfx, un paquete esbuild, eventos, setTick y Wait, y llamar a Lua exports.

Quieres tipos, async/await y paquetes npm en tu script de FiveM, pero tu recurso es solo archivos .lua. FiveM ejecuta JavaScript en su propio tiempo de ejecución, así que TypeScript funciona siempre y cuando se compile a JavaScript primero. Esta guía proporciona el diseño de funcionamiento: manifiesto, tipos, bundler, eventos y marcas, y cómo mezclarlo con Lua.

Diseño del proyecto

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

Editas src/, compilas en dist/, y el manifiesto solo sabe de dist/.

El fxmanifest

lua
fx_version 'cerulean'
game 'gta5'

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

Ambos son archivos JavaScript simples. Si tu recurso necesita una versión particular del tiempo de ejecución de JavaScript o funciones de Node en el servidor, verifica los documentos actuales de Cfx.re para las opciones del manifiesto, ya que los tiempos de ejecución admitidos han cambiado con el tiempo.

Consejo: si dejas fuera la compilación, FiveM dice que no puede encontrar dist/client.js. Siempre ejecuta la compilación antes de ensure o restart.

Instala tipos y un bundler

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

@citizenfx/client y @citizenfx/server son los paquetes de tipo para los nativos y funciones de Cfx.re en cada lado. Un tsconfig.json que los utiliza:

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

TypeScript solo verifica tipos aquí (noEmit). esbuild hace la agrupación 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"
  }
}

La agrupación significa que los paquetes npm que importas se copian en el archivo de salida único, por lo que el servidor no necesita node_modules en tiempo de ejecución. Verifica los documentos de Cfx.re para el --platform y --target correcto para el tiempo de ejecución que utilizan tus artefactos.

Eventos

JavaScript tiene los mismos cuatro bloques de construcción que Lua, con otros nombres:

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

En el servidor, el jugador que activó el evento está en el global source, como en Lua. Léelo en una constante al inicio del controlador: después de un await, el global puede apuntar a otro jugador. Las mismas reglas de seguridad de asegurar eventos del servidor se aplican, ya que el jugador puede disparar tu onNet desde afuera. Más sobre el modelo de eventos en eventos del cliente y el servidor.

Marcas y esperas

No hay un bucle de fotograma por defecto. setTick ejecuta una función cada fotograma, y Wait de Lua se convierte en una promesa alrededor 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
})

Sin un retraso, el controlador se ejecuta cada fotograma, el mismo problema que un bucle Lua sin Wait(0): ver Wait y hilos explicados. Usa un retraso más largo cuando nada necesita ser verificado cada fotograma.

Para un retraso única, omite setTick y solo await Delay(1000) dentro de una función asincrónica.

Mezclando con exports de Lua

Los exports funcionan entre lenguajes. Registra uno en JavaScript:

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

Y llámalo desde un recurso Lua:

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

La otra dirección, llamar a un export de Lua desde TypeScript, pasa por el objeto exports:

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

Declara la forma tú mismo si los tipos no conocen el otro recurso, por ejemplo con declare const exports: Record<string, any>. Ver exports en Lua para cómo se comportan cuando el recurso de destino se reinicia.

Compila antes de comenzar

Ejecuta el observador mientras trabajas, y reinicia el recurso en la consola después de cada compilación:

text
npm run watch
restart my_resource

Si un reinicio no hace nada nuevo, verifica que el archivo de salida cambió, que el manifiesto apunta a él, y que el recurso no se sirve desde una copia obsoleta en cache/.

Lista de verificación

Síntoma Solución
FiveM no puede encontrar dist/client.js Ejecuta la compilación; el manifiesto carga la salida, no src/
Los nativos aparecen como desconocidos en el editor Instala @citizenfx/client y @citizenfx/server
import falla en tiempo de ejecución Agrupa con esbuild para que los imports se inserten
El controlador utiliza el jugador incorrecto Lee source en una constante antes de cualquier await
Alto resmon de una marca Agrega await Delay(ms) al controlador setTick
Lua no puede ver tu función Regístrala con exports('name', fn) y llámala por nombre de recurso

Respuestas rápidas

¿Puedo usar TypeScript en un recurso de FiveM directamente?

No. FiveM ejecuta JavaScript. Escribes TypeScript, lo agrupes en JavaScript simple con una herramienta como esbuild, y apunta client_script y server_script a los archivos de salida.

¿Puede un recurso TypeScript llamar a exports de Lua?

Sí. Los exports se comparten entre lenguajes: llama exports['my_resource'].functionName() desde JavaScript, y Lua puede llamar a exports que registres con exports('name', fn) en JavaScript.

¿Qué reemplaza Wait() en JavaScript?

Usa una promesa alrededor de setTimeout dentro de una función asincrónica o un controlador setTick. El bucle de marcas en sí es setTick, y cada marca debe esperar un retraso para no ejecutarse cada fotograma.

Scripts que evitan este problema

Quest CreatorUn editor visual de misiones y diálogos con NPC, nodo a nodo dentro del juego.Ver script →Item Creator V2Crea items usables con animaciones, props, efectos y más — sin escribir código.Ver script →Shop CreatorCrea una tienda en menos de un minuto — dueños, empleados, caja fuerte y atracos.Ver script →

Sigue leyendo