Resources FiveM TypeScript : fxmanifest, types @citizenfx et esbuild

Écrivez des resources FiveM en TypeScript ou JavaScript : fxmanifest avec fichiers dist, types @citizenfx, un bundle esbuild, événements, setTick et Wait, et appeler les exports Lua.

Vous voulez des types, async/await et des packages npm dans votre script FiveM, mais votre resource n'est que des fichiers .lua. FiveM exécute JavaScript dans son propre runtime, donc TypeScript fonctionne tant qu'il est compilé en JavaScript d'abord. Ce guide donne la disposition de travail : manifest, types, bundler, événements et ticks, et comment le mélanger avec Lua.

Disposition du projet

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

Vous modifiez src/, construisez dans dist/, et le manifest ne connaît que dist/.

Le fxmanifest

lua
fx_version 'cerulean'
game 'gta5'

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

Les deux sont des fichiers JavaScript simples. Si votre resource a besoin d'une version particulière du runtime JavaScript ou de fonctionnalités Node sur le serveur, vérifiez la documentation actuelle de Cfx.re pour les options de manifest, car les runtimes supportés ont changé au fil du temps.

Conseil : si vous oubliez la construction, FiveM dit qu'il ne peut pas trouver dist/client.js. Exécutez toujours la construction avant ensure ou restart.

Installez les types et un bundler

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

@citizenfx/client et @citizenfx/server sont les packages de type pour les natives et les fonctions Cfx.re de chaque côté. Un tsconfig.json qui les utilise :

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

TypeScript vérifie uniquement le type ici (noEmit). esbuild effectue le regroupement réel :

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

Le regroupement signifie que les packages npm que vous importez sont copiés dans le fichier de sortie unique, de sorte que le serveur n'a pas besoin de node_modules au runtime. Vérifiez la documentation de Cfx.re pour le bon --platform et --target pour le runtime que vos artefacts utilisent.

Événements

JavaScript a les mêmes quatre éléments de base que Lua, avec d'autres noms :

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

Sur le serveur, le joueur qui a déclenché l'événement est dans le source global, comme en Lua. Lisez-le dans une constante au début du gestionnaire : après un await, le global peut pointer vers un autre joueur. Les mêmes règles de sécurité de sécurisation des événements serveur s'appliquent, car le joueur peut déclencher votre onNet de l'extérieur. Plus sur le modèle d'événement dans événements client et serveur.

Ticks et attente

Il n'y a pas de boucle de frame par défaut. setTick exécute une fonction à chaque frame, et Wait de Lua devient une promesse autour 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
})

Sans délai, le gestionnaire s'exécute à chaque frame, le même problème qu'une boucle Lua sans Wait(0) : voir Wait et threads expliqués. Utilisez un délai plus long quand rien n'a besoin d'être vérifiée à chaque frame.

Pour un délai ponctuel, ignorez setTick et just await Delay(1000) à l'intérieur d'une fonction async.

Mélange avec les exports Lua

Les exports fonctionnent entre les langages. Enregistrez-en un en JavaScript :

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

Et appelez-le à partir d'une resource Lua :

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

L'autre direction, appeler un export Lua depuis TypeScript, passe par l'objet exports :

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

Déclarez vous-même la forme si les types ne connaissent pas l'autre resource, par exemple avec declare const exports: Record<string, any>. Voir exports en Lua pour comment ils se comportent quand la resource cible redémarre.

Construisez avant de commencer

Exécutez le watcher pendant que vous travaillez, et redémarrez la resource dans la console après chaque construction :

text
npm run watch
restart my_resource

Si un redémarrage ne fait rien de nouveau, vérifiez que le fichier de sortie a changé, que le manifest le pointe, et que la resource n'est pas servie à partir d'une copie obsolète dans cache/.

Liste de contrôle

Symptôme Correction
FiveM ne peut pas trouver dist/client.js Exécutez la construction ; le manifest charge la sortie, pas src/
Les natives s'affichent comme inconnues dans l'éditeur Installez @citizenfx/client et @citizenfx/server
import échoue au runtime Regroupez avec esbuild pour que les imports soient inline
Le gestionnaire utilise le mauvais joueur Lisez source dans une constante avant tout await
Resmon élevé à partir d'un tick Ajoutez await Delay(ms) au gestionnaire setTick
Lua ne peut pas voir votre fonction Enregistrez-la avec exports('name', fn) et appelez-la par nom de resource

Réponses rapides

Puis-je utiliser TypeScript dans une resource FiveM directement ?

Non. FiveM exécute JavaScript. Vous écrivez TypeScript, le regroupez en JavaScript simple avec un outil comme esbuild, et pointez client_script et server_script vers les fichiers de sortie.

Une resource TypeScript peut-elle appeler les exports Lua ?

Oui. Les exports sont partagés entre les langages : appelez exports['my_resource'].functionName() depuis JavaScript, et Lua peut appeler les exports que vous enregistrez avec exports('name', fn) en JavaScript.

Qu'est-ce qui remplace Wait() en JavaScript ?

Utilisez une promesse autour de setTimeout à l'intérieur d'une fonction async ou d'un gestionnaire setTick. La boucle de ticking elle-même est setTick, et chaque tick devrait attendre un délai pour qu'il ne s'exécute pas à chaque frame.

Des scripts sans ce problème

Quest CreatorUn éditeur visuel de quêtes et de dialogues PNJ, construit nœud par nœud en jeu.Voir le script →Item Creator V2Créez des items utilisables avec animations, props, effets et plus — sans écrire une ligne de code.Voir le script →Shop CreatorCréez un magasin en moins d’une minute — propriétaires, employés, coffres et braquages inclus.Voir le script →

À lire aussi