Retour au cours

games / fivem

NUI callbacks : communication JS vers Lua

Leçon 71 exercice

Explication

Ce que vous allez apprendre

  • Enregistrer un callback NUI côté Lua avec RegisterNUICallback
  • Déclencher ce callback depuis JavaScript avec fetch()
  • Comprendre pourquoi cb(...) doit toujours être appelé, même avec un objet vide
  • Récupérer GetParentResourceName() pour cibler la bonne ressource depuis le JS
  • Revalider côté serveur toute action métier déclenchée par un clic dans l'interface

Dans quel contexte ?

Un joueur clique sur "Acheter" dans le menu de boutique construit en leçon 6 : ce clic, purement visuel côté JavaScript, doit déclencher une vraie transaction côté Lua (retirer de l'argent, ajouter l'objet). Sans NUI callback, la page web resterait une simple façade sans aucun effet réel sur le jeu — cette leçon relie enfin l'interface à la logique du script.

ÉlémentRôle
RegisterNUICallback(name, fn)Déclare un point d'entrée Lua appelable depuis le JS
fetch(...)Déclenche ce callback depuis la page web
cb(response)Réponse OBLIGATOIRE, sinon le fetch reste bloqué

Piège fréquent

Un RegisterNUICallback qui ne finit jamais par appeler cb(...) laisse la promesse JavaScript associée bloquée indéfiniment. Le symptôme typique côté joueur : l'interface semble "figée" après un clic, sans aucun message d'erreur visible pour comprendre pourquoi.

Le sens manquant de la communication NUI

La leçon précédente a montré comment le Lua envoie des données vers la page web (SendNUIMessage). Mais une interface interactive a aussi besoin du sens inverse : quand un joueur clique sur "Acheter" dans la page web, il faut que ce clic déclenche une action réelle côté Lua (retirer de l'argent, ajouter un objet). C'est le rôle des NUI callbacks.

Pourquoi passer par fetch()

JavaScript utilise ici fetch(), la fonction standard du web pour envoyer une requête HTTP — exactement comme un site web interrogerait un serveur classique. FiveM intercepte ces requêtes particulières (adressées à GetParentResourceName(), qui identifie la ressource courante) et les redirige vers le code Lua correspondant, enregistré au préalable avec RegisterNUICallback. Il n'y a donc pas de vraie requête réseau qui sort de la machine : c'est un mécanisme purement local entre la page et le script Lua qui l'héberge.

Le piège qui bloque tout : oublier de répondre

C'est le point le plus important à retenir de cette leçon. Le JavaScript, via fetch(), attend une réponse. Si le code Lua enregistré avec RegisterNUICallback ne répond jamais explicitement en appelant cb(...), la promesse JavaScript côté page reste bloquée indéfiniment en attente — un bug fréquent chez les débutants, qui se traduit par une interface qui semble "figée" après un clic, sans message d'erreur clair pour comprendre pourquoi.

Ne jamais faire confiance au clic

Un clic dans l'interface ne doit jamais, à lui seul, valider une action métier sensible. Exactement comme pour les events réseau classiques (vu en leçon 4 et détaillé dans la leçon Sécurité), le code déclenché par un callback NUI ne fait que relayer une intention côté serveur, qui reste seul juge de la validité réelle de l'action (prix, quantité, autorisations).

Commandes & code

NUI callbacks : communication JS vers Lua

La communication inverse (JS vers Lua) passe par des NUI callbacks, appelés via fetch().

lua
-- CLIENT : enregistre un callback NUI appelable depuis le JS
RegisterNUICallback('buyItem', function(data, cb)
    TriggerServerEvent('technologik:buyItem', data.itemName, 1)
    cb({ status = 'ok' }) -- répondre est OBLIGATOIRE, sinon le fetch JS reste bloqué en attente
end)

RegisterNUICallback('closeShop', function(data, cb)
    SetNuiFocus(false, false) -- rend le contrôle au jeu
    cb({})
end)
javascript
// html/ui.js
function buyItem(itemName) {
    fetch(`https://${GetParentResourceName()}/buyItem`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json; charset=UTF-8' },
        body: JSON.stringify({ itemName }),
    })
    .then(resp => resp.json())
    .then(result => {
        if (result.status === 'ok') {
            console.log('Achat confirmé');
        }
    })
    .catch(err => console.error('Erreur NUI callback:', err));
}

document.getElementById('close-btn').addEventListener('click', () => {
    fetch(`https://${GetParentResourceName()}/closeShop`, {
        method: 'POST',
        body: JSON.stringify({}),
    });
    document.getElementById('shop').classList.add('hidden');
});

// Fermer avec Échap, comme la plupart des menus FiveM
window.addEventListener('keyup', (e) => {
    if (e.key === 'Escape') {
        document.getElementById('close-btn').click();
    }
});
javascript
// html/ui.js : brancher les clics sur les items dynamiquement générés
function renderShopItems(items) {
    const list = document.getElementById('shop-items');
    list.innerHTML = '';

    items.forEach(item => {
        const li = document.createElement('li');
        li.textContent = `${item.name} - $${item.price}`;
        li.addEventListener('click', () => buyItem(item.name));
        list.appendChild(li);
    });
}

Résumé

  • GetParentResourceName() retourne le nom de la ressource, utilisé comme host dans le fetch.
  • Un RegisterNUICallback DOIT appeler cb(...), même avec un objet vide, sinon le JS reste en attente.
  • Toute action métier (achat, argent) doit être re-validée côté serveur, jamais décidée en JS.

Exercices pratiques

1 disponible
1

Mission : le clic 'Acheter' qui fige la boutique

Objectif : Corriger un callback NUI qui ne répond jamais et déplacer la logique d'achat côté serveur.

Contexte

Après avoir cliqué une seule fois sur "Acheter" dans le menu boutique, l'interface entière semble figée : plus aucun clic ne réagit, sans message d'erreur dans la console F8. Le code en cause :

lua
RegisterNUICallback('buyItem', function(data, cb)
    RemoveMoney(source, data.price)
    AddItem(source, data.itemName, 1)
end)

Le prix (data.price) est directement envoyé par le JavaScript, calculé côté page web.

Résoudre l’exercice →