Risorse FiveM TypeScript: fxmanifest, tipi @citizenfx ed esbuild
Scrivi risorse FiveM in TypeScript o JavaScript: fxmanifest con file dist, tipi @citizenfx, un bundle esbuild, eventi, setTick e Wait, e chiama le esportazioni Lua.
Vuoi tipi, async/await e pacchetti npm nel tuo script FiveM, ma la tua risorsa è solo file .lua. FiveM esegue JavaScript nel suo runtime proprio, quindi TypeScript funziona purché sia compilato in JavaScript prima. Questa guida fornisce il layout funzionante: manifest, tipi, bundler, eventi e tick, e come mescolarlo con Lua.
Layout del progetto
my_resource/
fxmanifest.lua
package.json
tsconfig.json
src/
client.ts
server.ts
dist/ (build output, loaded by FiveM)Modifichi src/, costruisci in dist/, e il manifest conosce solo dist/.
Il fxmanifest
fx_version 'cerulean'
game 'gta5'
client_script 'dist/client.js'
server_script 'dist/server.js'Entrambi sono file JavaScript semplici. Se la tua risorsa ha bisogno di una particolare versione del runtime JavaScript o delle funzioni Node sul server, controlla la documentazione Cfx.re corrente per le opzioni del manifest, poiché i runtime supportati sono cambiati nel tempo.
Consiglio: se lasci fuori la build, FiveM dice che non può trovare
dist/client.js. Esegui sempre la build prima diensureorestart.
Installare tipi e un bundler
npm init -y
npm install --save-dev typescript esbuild @citizenfx/client @citizenfx/server@citizenfx/client e @citizenfx/server sono i pacchetti di tipo per i nativi e le funzioni Cfx.re su ogni lato. Un tsconfig.json che li utilizza:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}TypeScript qui solo controlla il tipo (noEmit). esbuild fa il raggruppamento effettivo:
{
"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"
}
}Raggruppare significa che i pacchetti npm che importi vengono copiati nel singolo file di output, quindi il server non ha bisogno di node_modules in fase di runtime. Controlla la documentazione Cfx.re per il giusto --platform e --target per il runtime che i tuoi artefatti utilizzano.
Eventi
JavaScript ha gli stessi quattro blocchi di costruzione di Lua, con altri nomi:
| 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)
})Sul server, il giocatore che ha attivato l'evento è nel source globale, come in Lua. Leggilo in una costante all'inizio dell'handler: dopo un await, il globale potrebbe indicare un altro giocatore. Le stesse regole di sicurezza da securing server events si applicano, poiché il giocatore può far scattare il tuo onNet da fuori. Ulteriori informazioni sul modello di evento in client e server eventi.
Tick e attesa
Non c'è un frame loop per impostazione predefinita. setTick esegue una funzione ogni frame, e Wait da Lua diventa una promise attorno a 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
})Senza un ritardo, l'handler esegue ogni frame, lo stesso problema di un loop Lua senza Wait(0): vedi Wait e thread spiegati. Usa un ritardo più lungo quando nulla ha bisogno di essere verificato ogni frame.
Per un ritardo una tantum, salta setTick e await Delay(1000) dentro una funzione async.
Mescolamento con esportazioni Lua
Le esportazioni funzionano tra le lingue. Registrane una in JavaScript:
exports('getGreeting', (name: string) => `Hello ${name}`)E chiamala da una risorsa Lua:
local text = exports['my_resource']:getGreeting('Mic')L'altra direzione, chiamare un'esportazione Lua da TypeScript, attraversa l'oggetto exports:
const ok = exports['ox_inventory'].GetItemCount(source, 'bread')Dichiara la forma te stesso se i tipi non conoscono l'altra risorsa, ad esempio con declare const exports: Record<string, any>. Vedi esportazioni in Lua per come si comportano quando la risorsa target si riavvia.
Build prima di iniziare
Esegui il watcher mentre lavori, e riavvia la risorsa nella console dopo ogni build:
npm run watch
restart my_resourceSe un restart non fa nulla di nuovo, controlla che il file di output sia cambiato, che il manifest lo indichi, e che la risorsa non sia servita da una copia stale in cache/.
Elenco di controllo
| Sintomo | Correzione |
|---|---|
FiveM non può trovare dist/client.js |
Esegui la build; il manifest carica l'output, non src/ |
| I nativi appaiono come sconosciuti nell'editor | Installa @citizenfx/client e @citizenfx/server |
import fallisce in fase di runtime |
Raggruppa con esbuild in modo che gli import siano inline |
| L'handler usa il giocatore sbagliato | Leggi source in una costante prima di qualsiasi await |
| Resmon alto da un tick | Aggiungi await Delay(ms) all'handler setTick |
| Lua non può vedere la tua funzione | Registrala con exports('name', fn) e chiamala per nome di risorsa |
Risposte rapide
Posso usare TypeScript in una risorsa FiveM direttamente?
No. FiveM esegue JavaScript. Tu scrivi TypeScript, lo raggruppi in JavaScript semplice con uno strumento come esbuild, e punti client_script e server_script ai file di output.
Una risorsa TypeScript può chiamare esportazioni Lua?
Sì. Le esportazioni sono condivise tra i linguaggi: chiama exports['my_resource'].functionName() da JavaScript, e Lua può chiamare le esportazioni che registri con exports('name', fn) in JavaScript.
Cosa sostituisce Wait() in JavaScript?
Usa una promise attorno a setTimeout dentro una funzione async o un handler setTick. Il loop di tick stesso è setTick, e ogni tick dovrebbe aspettare un ritardo in modo che non funzioni ogni frame.
Script senza questo problema
Quest CreatorUn editor visuale di missioni e dialoghi con gli NPC, costruito nodo per nodo in gioco.Vedi script →
Item Creator V2Crea oggetti utilizzabili con animazioni, props, effetti e altro — senza scrivere codice.Vedi script →
Shop CreatorCrea un negozio in meno di un minuto — proprietari, dipendenti, casseforti e rapine inclusi.Vedi script →