Inventaires techniques¶
item_catalog_entries.system_managed protège vehicle_key dans le catalogue et les services
génériques. La table vehicle_keys est autoritative ;
InventoryItem.metadata_json.vehicle_key_id n’est qu’un pointeur. Les transferts conservent l’UUID
de l’instance. Plusieurs doubles du même véhicule peuvent appartenir au même inventaire : les
contrôles de possession recherchent l'existence d'au moins une instance active, mais le chemin
d'utilisation d'un item transmet et valide toujours son UUID exact, sans fallback vers un autre
double.
building_badge est créé uniquement par le service immobilier avec allow_system=True. Les
transferts conservent l'UUID lié au badge et les contrôles de porte résolvent son porteur actuel.
Voir Architecture technique de l'immobilier.
L'entrée catalogue vehicle_key référence vehicle_key.png. Le même PNG 100 × 100 transparent
est embarqué dans medusa-inventory/web/images/vehicle_key.png pour les NUI FiveM et dans
admin/public/item-icons/vehicle_key.png pour /item-icons/vehicle_key.png dans la console web.
Les deux copies doivent rester binaires identiques afin que les inventaires affichent une silhouette
contrastée cohérente dans leurs cases carrées et leurs panneaux de détail.
Stockages persistants¶
La migration 0022_inventory_storages relie chaque ligne inventory_storages à un inventaire de
catégorie storage. Elle conserve position, orientation, modèle et mode du prop (généré ou lié),
tandis que inventories porte le nom, les slots, le poids, les permissions et la révision.
Les routes internes /internal/v1/inventory-storages assurent CRUD, ouverture, transfert et
réorganisation. À l'ouverture, les permissions requises de l'inventaire sont comparées aux
permissions effectives transmises par FiveM ; une liste vide rend le stockage public. Les
transferts verrouillent les inventaires personnel et partagé dans l'ordre des UUID, contrôlent les
deux révisions et les deux poids puis déplacent ou échangent les instances dans une seule
transaction. Le déplacement interne verrouille pareillement le stockage et son slot cible.
medusa-storages possède la synchronisation des props, leur interaction et les commandes World
Edit. Une ouverture est d'abord revalidée côté serveur à proximité des coordonnées persistées,
puis transmise par un événement serveur local à medusa-inventory; le client ne peut donc pas
ouvrir arbitrairement un UUID distant. medusa-inventory conserve une session typée drop ou
storage, utilise les endpoints correspondants et réemploie sans duplication sa NUI droite.
La sauvegarde de placement suit un acquittement medusa:storages:saveResult. Le client garde le
workflow en attente jusqu'à la réponse ou dix secondes, puis notifie et rouvre World Edit. Le
serveur journalise le payload refusé ou la pile API, et publie le résultat dans F9. La création API
recharge explicitement l'inventaire et sa collection d'items après commit afin d'éviter tout lazy
load asynchrone pendant la sérialisation. Enfin, la synchronisation périodique compare le snapshot
API et rediffuse seulement lors d'un changement, ce qui récupère une écriture validée même si sa
réponse initiale a été interrompue sans faire scintiller les props toutes les trois secondes.
Dépôts persistants¶
inventory_drops relie coordonnées et état à un inventaire drop. inventory_drop_runtime mémorise un jeton de démarrage FiveM : un nouveau jeton supprime les anciens inactifs et inactive les actifs. Un simple redémarrage de ressource ne fait pas avancer ce cycle.
Un transfert verrouille les deux inventaires dans l'ordre de leurs UUID, contrôle leurs révisions puis déplace l'instance dans la même transaction. Seuls les dépôts actifs contenant au moins une instance sont publiés aux clients et matérialisés par un prop. medusa-inventory resynchronise après chaque transfert et périodiquement pour propager les changements sans redémarrage.
Le chemin critique d'un transfert sérialise désormais directement les deux inventaires déjà verrouillés, avec un seul chargement partagé du catalogue. Il ne relit plus le personnage, le dépôt et leurs objets après le commit. FiveM diffuse ensuite uniquement le dépôt modifié ; la synchronisation exhaustive périodique reste le filet de sécurité. L'interface du dépôt réemploie les classes de la console personnelle et limite sa feuille dédiée aux différences de position et d'identité visuelle.
La suppression côté client appelle l'export unregister de medusa-interactions dans un appel protégé, puis reprend explicitement la propriété de l'objet avant DeleteObject, avec DeleteEntity en secours. Une erreur de nettoyage de l'interaction ne peut donc plus empêcher le retrait du prop.
La création transmet drop.slots depuis la configuration FiveM, avec 25 comme valeur de secours. Les transferts transportent target_slot jusqu'à l'API. Sous verrou, celle-ci contrôle la borne, calcule le poids résultant des deux inventaires et réalise un déplacement ou un échange atomique. Lors d'un échange, l'objet cible passe temporairement dans un emplacement technique afin de respecter les contraintes d'unicité SQL, puis rejoint la case source libérée. Les anciens appels sans emplacement conservent le placement automatique au premier emplacement libre.
Le déplacement interne utilise POST /internal/v1/inventory-drops/{id}/move avec l'instance, la case cible et la révision attendue. L'inventaire du dépôt est verrouillé ; une cible occupée est échangée avec le même mécanisme d'emplacement temporaire. Après commit, FiveM diffuse dropInventoryChanged afin de rafraîchir uniquement les interfaces actuellement ouvertes sur ce dépôt.
Catalogue dynamique¶
La migration 0019_dynamic_item_catalog crée et initialise item_catalog_entries avec les dix
objets historiques. PostgreSQL est la source de vérité du catalogue ; les lectures d'inventaire
résolvent les définitions dans cette table.
Les routes web GET/POST /admin/items et DELETE /admin/items/{item_id} appliquent OAuth, CSRF et
respectivement ADMIN_ALLOW_ITEM_CREATE ou ADMIN_ALLOW_ITEM_DELETE. La suppression efface les
instances portant cet item_id dans la même transaction et incrémente la révision de chaque
inventaire touché. Les images acceptent un nom de fichier embarqué ou une URL HTTP(S), reconnue par
les interfaces web, l'inventaire joueur et l'admin en jeu.
La migration 0020_item_catalog_categories ajoute category, indexe cette colonne et classe les
dix définitions initiales. PUT /admin/items/{item_id}, protégé par ADMIN_ALLOW_ITEM_EDIT, met à
jour les champs éditables sans changer la clé référencée par inventory_items.item_id. Comme la
sérialisation résout la définition depuis PostgreSQL à chaque lecture, toutes les instances
reçoivent immédiatement les nouvelles métadonnées. Quand la NUI joueur reste ouverte,
medusa-inventory demande silencieusement un état autoritatif toutes les cinq secondes.
Données et catalogue¶
La migration 0018_inventories crée inventories et inventory_items. L'inventaire personnel est
créé paresseusement avec une contrainte unique sur character_id et est supprimé en cascade avec
le personnage. Un item est une instance UUID liée à un slot unique ; il conserve item_id, label,
quantité et métadonnées. Le catalogue immuable est validé depuis api/app/item_catalog.json et
répliqué dans la configuration FiveM pour l'affichage.
Les limites personnelles sont synchronisées depuis medusa-inventory/config.json à l'ouverture. Les
valeurs par défaut API restent 25 slots et 15 kg afin que les interfaces administratives puissent
créer/lire un inventaire avant la première connexion du personnage.
Cohérence et concurrence¶
PostgreSQL est la vérité. Toute mutation charge la ligne inventories avec SELECT … FOR UPDATE,
effectue l'ensemble de la modification dans une transaction puis incrémente revision. Les clients
transmettent la révision connue pour les déplacements/éditions ; une révision obsolète retourne
409 inventory_conflict au lieu d'écraser un état plus récent. L'échange de slots utilise un slot
temporaire dans la même transaction afin de respecter l'unicité (inventory_id, slot).
Cette discipline est utilisée par les stockages partagés et les drops, et constitue la base des
futurs coffres et réfrigérateurs.
Les clients ne modifient jamais un inventaire localement de manière définitive : ils rendent la
réponse autoritative de l'API. /giveitem utilise le même verrou mais passe intentionnellement le
contrôle de capacité ; les slots supplémentaires sont numérotés au-delà de la limite configurée.
Le service d'ajout normal refuse inventory_full ou inventory_too_heavy avant toute mutation ;
seul le flux administratif active explicitement le bypass.
Resource FiveM¶
medusa-inventory dépend de medusa-core et medusa-player-utils. Son serveur résout exclusivement le
personnage actif depuis la source FiveM avant tout appel API. Le client enregistre inventory sur
Tab et cinq commandes inventoryslot1 à inventoryslot5, que le joueur peut remapper. La NUI
reste transparente hors ouverture, utilise cinq colonnes et le langage visuel AR Medusa.
Le document NUI force explicitement html, body et l'état [hidden] à rester transparents ; les
surfaces sombres sont limitées au bandeau, à la matrice et au panneau de détail afin de ne jamais
produire un backdrop opaque à l'échelle de l'écran.
La console est limitée à 46vw et ancrée à gauche. La matrice et l'analyse objet restent dans ce
même conteneur ; sur les résolutions étroites, l'analyse est masquée pour préserver cinq colonnes
sans étendre l'interface vers la moitié droite. Les labels d'instance sont injectés avec
textContent et tronqués visuellement, avec le nom complet conservé dans le titre et le libellé
accessible du slot.
Chaque .slot impose aspect-ratio: 1 / 1. Les lignes CSS Grid sont dimensionnées automatiquement
depuis la largeur des cinq colonnes et align-content: start empêche l'espace vertical disponible
d'étirer les cellules ; le conteneur garde son défilement vertical en cas de dépassement.
L'utilisation valide d'abord que l'instance ou le slot appartient encore à l'inventaire, puis émet
medusa:inventory:itemUsed côté client et l'événement local medusa:inventory:useItem. Aucun effet
ou retrait automatique n'est défini dans cette version.
Les routes internes /internal/v1/inventories exigent la clé API. Les routes administratives
réévaluent les rôles transmis pour chaque requête. Les routes web /admin/inventories utilisent
Discord OAuth, CSRF et les permissions effectives habituelles.
Le déplacement joueur utilise les événements Pointer plutôt que le DnD HTML natif, dont le support
est irrégulier dans le Chromium NUI. Après un seuil de six pixels, un fantôme visuel suit le
curseur et elementFromPoint résout explicitement n'importe quel .slot sous celui-ci, occupé ou
vide. La destination transmet le slot et la révision connue ; l'API échange atomiquement les deux
slots lorsqu'il est occupé. Le clic et le double clic sont inhibés à la fin d'un déplacement.
POST /admin/inventories/{character_id}/items et l'équivalent interne /admin/add ajoutent une
référence du catalogue avec contrôle de révision et bypass administratif des capacités. Les menus
contextuels convertissent un retrait partiel en mise à jour de quantité et suppriment l'instance
si la quantité retirée atteint la pile entière. Toutes les routes réévaluent
ADMIN_ALLOW_EDIT_INVENTORY côté serveur.
Les fiches personnages web et NUI ne dupliquent aucun composant d'inventaire. Elles transmettent le
personnage déjà sélectionné au chargeur existant, puis activent respectivement inventories-panel
ou la vue inventories. Les endpoints et contrôles ADMIN_ALLOW_VIEW_INVENTORY / édition restent
donc identiques quel que soit le point d'entrée.
Au onResourceStart, le serveur calcule une seule fois son boot_token, puis appelle la
réconciliation idempotente avec un retry borné. Toutes les tentatives réutilisent le même token.
Les synchronisations périodiques de drops restent à tentative unique et les logs utilisent le
format d'erreur partagé.
Résolution contextuelle de TAB¶
Le registre client des contextes trie les entrées par priorité puis ID. Il n'accepte comme callback
qu'une fonction Lua locale ou une référence Cfx stricte : table contenant
__cfx_functionReference et métatable dont __call est une fonction. Une table ordinaire, une
pseudo-référence incomplète ou nil est refusé sans créer d'entrée partielle. L'invocation passe par
pcall; une exception produit un diagnostic borné et laisse le registre utilisable.
resolveInventoryContext appelle chaque résolveur au plus une fois. Un résultat
{handled=true, message=...} consomme TAB même sans événement d'ouverture ; le fallback
personnel/drop n'est exécuté que si aucun résolveur n'a trouvé ou refusé de contexte. Cette
distinction empêche un coffre ambigu, verrouillé ou autrement invalide d'être masqué par l'ouverture
du stockage personnel.
medusa-vehicles inscrit son entrée avec un ID stable et une priorité explicite. L'inscription et
la désinscription sont idempotentes, couvrent les deux ordres de démarrage, le restart de
medusa-inventory et le stop de la ressource véhicules. Les retries sont bornés et un échec n'est
journalisé qu'au changement d'état, jamais à chaque frame ou appui.
Le résolveur distingue quatre états de capacité : ready, pending, unsupported et disabled.
pending ne déclenche qu'une demande de réparation par network ID pendant cinq secondes. En
extérieur, la recherche géométrique classe d'abord les cibles et n'émet la demande que pour le
véhicule sélectionné, afin d'éviter un événement par véhicule dans un parking. Une place avant ou
une zone arrière plausible en attente renvoie handled=true avec un message français ; les sièges
arrière, le flag désactivé et l'absence de cible renvoient nil pour conserver le fallback normal.
Primitive transactionnel commun¶
api/app/services/inventory_transfers.py est l'unique primitive de transfert utilisée par dépôts,
stockages et compartiments véhicule. Il charge les deux inventaires sous verrou dans l'ordre de
leurs UUID, revalide l'instance source, les révisions, restrictions catalogue, slots, pile et poids,
puis applique déplacement ou échange dans une transaction. Le résultat contient les deux snapshots
et les avertissements fonctionnels. Les adaptateurs drop/storage conservent leurs codes d'erreur et
leurs réponses historiques.
L'avertissement de dernière clé n'est émis que si l'objet est une clé active du véhicule cible, qu'il ne reste aucune autre clé active et que la destination est son coffre ou sa boîte à gants. Une clé d'un autre véhicule ne produit pas de faux avertissement.
Sessions véhicule¶
Les sessions coffre/boîte à gants sont stockées temporairement dans Redis, avec identité personnage, inventaires, type, cible et expiration. Chaque transfert revalide en plus côté FiveM la même entité, le routing bucket, la zone physique, la vitesse, le verrou ou le siège avant. Une révision obsolète produit un conflit récupérable et un snapshot frais, sans retry automatique d'une mutation.
Les inventaires PNJ sont des lignes PostgreSQL temporaires afin de permettre une transaction sûre
pendant la vie de l'entité ; reconcile et forget suppriment explicitement la ligne et son
inventaire lorsque l'identité serveur n'est plus vivante. Ils ne génèrent jamais de loot. La
conversion PNJ vers UUID réutilise les mêmes instances d'objets et échoue intégralement si la grille
enregistrée est plus petite.
Poids dynamique énergie¶
effective_item_weight résout le bidon universel de carburant de 2 à 14 kg. Une metadata incorrecte
échoue au poids plein. Le bidon conserve un seul produit thermique à la fois et n'en change qu'à vide.
Le pack électrique pèse 8 kg, est non empilable et n'est consommé qu'après une livraison confirmée
de 10 points sur une batterie admissible. La migration 0040 réactive ces deux définitions sans
ressusciter les anciens objets supprimés en 0039. Voir Énergie des véhicules.