Aller au contenu

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.