Aller au contenu

Architecture multicarburant LC Fuel

Ownership et frontières

Le domaine est volontairement partagé sans double écriture :

  • PostgreSQL et l’API Medusa possèdent catalogue, réservoirs persistants, prix, stations, sessions, paiements, préférences, audit et rétention ;
  • medusa-energy valide le runtime FiveM, orchestre les intentions et publie les state bags ;
  • medusa-vehicles possède l’identité, le cycle de vie, le niveau canonique en pourcentage et les règles moteur/garage ;
  • medusa-banking possède les réservations, débits et remboursements ;
  • medusa-inventory possède les instances de bidon et de pack ;
  • lc_fuel fournit NUI, composition visuelle et assets streamés ;
  • lc_utils fournit uniquement son contrat et ses assets browser utiles.

La façade LC annonce writer=false, database=false, commands=false. Les scripts économiques, mysql-async, commandes slash, mises à jour automatiques et intégrations framework amont ne sont pas chargés. medusa-energy est l’unique writer de niveau. Le préflight bloque si une ressource de la denylist fuel tourne ou si le contrat LC est absent/incompatible après son démarrage.

Ordre attendu :

medusa-vehicles -> lc_utils -> medusa-energy -> lc_fuel -> medusa-admin

medusa-energy tolère l’absence transitoire de lc_fuel pendant le bootstrap, mais aucune session physique ne doit s’ouvrir avant que la façade soit disponible. L’image FiveM embarque aussi les deux façades pinées sous [mods]. À chaque démarrage de l’image, entrypoint.sh les remplace explicitement dans le volume persistant avant le fallback cp -Rn : ce dernier ne copie que les chemins absents et ne peut pas mettre à niveau un contrat LC existant. Cette synchronisation empêche notamment un writer R3 exigeant le contrat v2 de rencontrer une ancienne façade v1, situation fail-closed qui publierait volontairement zéro station.

Blips de station et navigation

medusa-energy/client/main.lua crée chaque blip de station avec SetBlipAsShortRange(blip, true). Le filtre de compte blip_mode détermine les stations créées (all, nearest par catégorie ou hidden), pas leur portée sur la minimap. Le moteur GTA gère la proximité sur le radar ; les blips restent présents sur la grande carte. Ne pas désactiver ce réglage dans le mode all, sinon les stations distantes s'accumulent au bord de la minimap.

Les recréations via registerStations, notamment après medusa:energy:preferenceResult, appliquent le même réglage aux catégories Route, Électrique, Aviation et Maritime. Sprites, couleurs, stations actives/archivées et feature flags sont inchangés. Aucun rayon personnalisé, scan global de blips, timer ou appel réseau n'est ajouté. Le GPS F5 reste indépendant : setGpsForCurrentVehicle choisit une station compatible puis appelle SetNewWaypoint, même si sa destination est hors radar.

Les nombres issus de MessagePack peuvent être des entiers Lua : tonumber(265) reste un entier. Les helpers privés de main.lua et medusa-admin/client.lua valident type, finitude et bornes du contrat (X/Y ±100 000, Z -10 000 à 100 000), puis font + 0.0 avant les natives flottantes. Zéro est accepté. Les stations invalides sont exclues avant création des blips, sélection du plus proche et GPS. AddBlipForCoord et le GPS F5 ne reçoivent plus les bits d'un entier à la place d'un flottant ; aucune coordonnée source Pillbox n'est modifiée.

F10 utilise les handlers partagés medusa:admin:vehicleWaypoint et medusa:admin:teleportToCoordinates. Le second normalise aussi les arguments de collision et de téléportation avant fade/gel. Un échec conserve l'accusé négatif existant. Les coordonnées restent résolues et autorisées côté serveur ; les droits énergie/véhicules/garages et ADMIN_ALLOW_TPC ne changent pas. Le spawn des points normalise leur heading fini dans [-360,360] sans le tourner. Les tests exécutent ces fonctions de production sous Lua 5.4 avec math.type dans les espions.

Modèle de données 0040, calibrations 0041 et orientations 0042

registered_vehicles.fuel_level reste le pourcentage canonique afin de préserver véhicules, garages et HUD. La migration 0040_vehicle_lc_fuel ajoute :

  • vehicle_energy_tank_states : vehicle_id 1:1, propulsion, capacité entière, parts Regular/Plus/Premium en ppm, multiplicateur, source catalogue, révision et snapshot ;
  • vehicle_energy_point_products : produits et débits disponibles par point ;
  • vehicle_energy_retention_runs : exécutions idempotentes de la politique 90/365 jours ;
  • aux stations/points : seed_key, origin, protected, catégorie physique et calibration JSON ;
  • aux sessions : produit, propulsion, unité, composition initiale et révision de prix ;
  • aux préférences : blip_mode (all, nearest, hidden).

Contraintes principales : capacité positive, propulsion dans gasoline|diesel|electric, somme des trois parts essence exactement égale à 1 000 000 pour un réservoir essence et nulle sinon, facteur de consommation borné, produit et unité cohérents, clé seed unique et révision strictement positive. Les quantités et coûts sont calculés en entiers ; les flottants sont limités aux entrées/sorties UI.

0041_vehicle_fuel_ports ajoute vehicle_energy_port_calibrations : UUID, nom de modèle normalisé, hash GTA unsigned, source lc_fuel|admin, offsets locaux right/forward/up, rotations optionnelles, distance, activation, protection, provenance, justification, révision et timestamps. Le snapshot embarqué dans api/alembic/data/0041_vehicle_fuel_ports.json contient exactement 669 entrées issues du bloc Config.HiddenCustomVehicleParameters du commit LC Fuel piné. Le SHA-256 canonique de ce bloc source est 6adfb8976c5154fa965777e956f66f3111716707249dba0bcb70a7f34d65feab.

Les lignes lc_fuel sont protégées par contraintes et trigger PostgreSQL : elles ne peuvent être ni modifiées ni supprimées. Les collisions nom/hash entre sources, noms non normalisés, distances et offsets hors bornes sont refusés. Une ligne admin masque la baseline du même modèle ; le reset supprime uniquement cette ligne.

0042_energy_charger_heading est une migration de données forward-only depuis 0041. Elle contient une table fermée des 26 couples heading 0040 -> (heading + 180) % 360 et met à jour séparément vehicle_energy_stations et vehicle_energy_points seulement si seed_key, origin=seed, protected=true et l’ancien angle correspondent encore, avec une tolérance de 0,0001°. Les coordonnées, produits, révisions et autres champs restent inchangés. Une rotation F10 divergente est donc volontairement ignorée. Le catalogue maintenu vehicle_energy_seeds.py porte les angles 0042 et constitue la source de restauration ; pump_detection.lua applique directement le heading persisté sans ajouter une deuxième rotation.

0043_energy_station_anchors ajoute uniquement des données et de l'audit, depuis 0042 :

  • table figée des 27 centres LC au commit 405c376529a48c8aa25c1553ca7e7ee6469a5b8c ;
  • Paleto Bay : UUID legacy exact, origine/protection/catégorie/bucket/révision/XYZ initiaux, activation et absence d'archive/session active exigés ; seuls XYZ, révision et date changent ;
  • sessions détectées par point_id -> station_id ou context_json.station_id, pour les cinq états actifs, y compris recovery_pending ; pas de colonne station_id sur les sessions ;
  • LSIA : décision auditée skipped_live_verification_missing, aucune désactivation ;
  • candidats lc-road-05,06,07,08,09,11,12,13,14,15,18,20,21,22,26,27 : UUID5 stable, bucket 0, public, rayon 80 m, sans surcharge ni point/prop supplémentaire ;
  • rejet si UUID, clé ou nom existent, ou si une ancre routière du même bucket couvre le centre à 80 m en 3D, y compris custom/privée/désactivée/archivée. Aucun réveil ni écrasement ;
  • vehicle_energy_seeds.py conserve les mêmes centres/noms pour la restauration explicite des seuls nouveaux seeds routiers ; point_seed_defaults renvoie None pour ces clés.

La transaction verrouille stations, sessions et événements en SHARE ROW EXCLUSIVE, avec attente maximale de 5 s. Chaque candidat possède un témoin d'audit déterministe permanent : appliqué ou ignoré avec motif, avant/après et nombre de lignes modifiées. Le rejeu conserve ce témoin et ne reconsidère pas un refus ni une édition F10 ultérieure. Les anciennes migrations sont immuables.

Résolution serveur

L’ordre de résolution est :

  1. anomalie de migration non résolue : blocage ;
  2. override véhicule actif ;
  3. état de réservoir persistant ;
  4. entrée catalogue modèle ;
  5. liste EV ou Diesel pinée ;
  6. entrée catalogue catégorie ;
  7. fallback sûr de catégorie avec diagnostic.

Les catégories actives sont land, motorcycle, quad, boat, plane, helicopter. bicycle, train, special et toute catégorie inconnue sont rejetés. La capacité native ne peut qu’ajuster une capacité thermique déjà classée ; une valeur client ne choisit jamais la propulsion.

Pour les véhicules persistants, ensure_tank_state matérialise ou verrouille l’état 1:1. Pour un PNJ, le serveur crée une projection state bag entre 20 et 80 % liée au couple boot/network ID ; aucune ligne registered_vehicles ou vehicle_energy_tank_states n’est créée.

Produits, mélange et coût

La matrice autorisée est stricte :

gasoline -> regular | plus | premium
diesel   -> diesel
electric -> electric_normal | electric_fast

Le mélange essence recalcule les trois parts en ppm par moyenne pondérée. Le reste entier est attribué au produit livré afin que la somme reste exactement 1 000 000. Le multiplicateur effectif est la moyenne pondérée de 1 000 000, 900 000 et 800 000 ppm. Diesel vaut 1 000 000 ppm.

Les produits thermiques utilisent 1 000 milliunités par litre. Les produits électriques utilisent 10 000 milliunités par point de pourcentage. Le coût brut est conservé en millicents. La clôture applique l’Option B : quantité nulle = 0 $, coût brut positif inférieur à 1 $ = 1 $, sinon ceil(coût en dollars). Une surcharge de station en points de base est appliquée en entiers avant la projection.

Le calcul du budget cherche la quantité entière maximale dont le plafond reste inférieur ou égal au budget. Il tient compte du plancher rationnel de raw_cost_millicents, ce qui évite de perdre des fractions livrables pour les unités électriques.

Sessions, verrous et paiement

Une intention durable porte un request_id unique. Des indexes partiels garantissent au plus une session active par personnage, véhicule, runtime ID, point et connecteur. Deux points différents restent parallèles. La durée d’inactivité est 120 secondes.

Cycle simplifié : validation physique → calcul de cible → réservation maximale → distribution par ticks monotones → confirmation du niveau → règlement final et remboursement. Le prix, l’unité, la source de paiement et la composition initiale sont figés dans la session. Un tick dupliqué retourne l’état courant ; une séquence sautée est refusée. Aucun tick ne précède une réservation réussie.

cash et account utilisent le service bancaire. portable ne débite pas d’argent à l’usage. Une annulation ou panne facture seulement confirmed_milliunits. Les états closing|recovery_pending permettent au scheduler de finaliser exactement une fois les paiements incomplets. Une panne API/banque bloque toute nouvelle opération au lieu de livrer gratuitement.

Validation physique et équipements

Les sept modèles GTA sont détectés par un scanner borné et quantifiés à 0,1 m. Le client envoie une observation ; le serveur reconstruit la clé canonique depuis station, bucket, modèle et coordonnées, puis l’API contrôle fraîcheur, rayon, allowlist et déduplication.

Les points seed portent un UUID stable. Les 26 chargeurs proposent deux produits électriques avec câble 7,5 m. Les 14 points spéciaux proposent les quatre thermiques avec câble 14 m. Les familles de véhicules sont contrôlées à la fois dans FiveM et l’API : route pour terrestres, aviation pour avions/hélicoptères, maritime pour bateaux.

Le résolveur commun charge d’abord l’entrée effective admin-over-LC du modèle. Une ligne source=admin contient le point complet dans le repère local de l’origine du véhicule (right=x, forward=y, up=z). Une ligne source=lc_fuel contient un delta depuis le premier bone sûr disponible dans l’ordre exact de vehicleCapBoneList() de la copie LC pinée : petrolcap, petroltank, petroltank_l, petroltank_r, wheel_lr, wheel_lf, engine, chassis_dummy. Ce delta est projeté avec la matrice du véhicule. Les ancres roue/moteur/châssis ne sont admises qu’après correspondance d’une baseline LC exacte par hash ; elles ne constituent jamais un fallback géométrique générique. Le point monde obtenu est reconverti une fois en coordonnées locales canoniques et devient l’unique entrée de la proximité, du ciblage, de la validation, de F10 et de l’attache physique. Sans entrée LC exploitable, le fallback brut est limité à petrolcap|petroltank|petroltank_l|petroltank_r, puis à la calibration historique du point. L’origine et le centre du véhicule ne sont jamais utilisés comme trappe. GetEntityModel est converti en hash unsigned avant la recherche ; une rotation absente reste NULL et les rotations LC plus profil physique ne sont composées qu’une fois.

La liste LC_FUEL_ANCHOR_BONES est gardée séparée de RAW_FUEL_BONES et testée contre la fonction amont pinée pour empêcher une nouvelle troncature. CAL-FUEL-PORT reste une erreur visible et localisée, mais le client mémorise uniquement la dernière clé code + modèle + connecteur pendant 2 500 ms. Ce cache constant ne crée ni timer, ni boucle, ni réseau par frame ; une autre cible ou l’expiration autorise immédiatement un nouveau message.

Pompes GTA virtuelles sans pointId, points seed/custom/spéciaux et objets portables passent tous par ce même résolveur. Le catalogue effectif admin > LC est conservé dans un index par hash hors des boucles frame. Il ne dépend pas du bootstrap personnel. Celui-ci attend le spawn réel, utilise le compte déjà enregistré sans exiger le drapeau entered, puis réessaie silencieusement selon 250/500/1 000/2 000/4 000 ms si le profil n’est pas encore disponible. Un restart client en jeu est détecté par medusaCharacterSpawned. L’état et la dernière erreur non personnelle sont exposés dans le diagnostic client, sans notification rouge pendant la sélection du personnage.

Le handshake global utilise medusa:energy:fuelPortsRequest puis une réponse ciblée medusa:energy:fuelPorts de schéma 2. La requête contient un identifiant, l’empreinte et le nombre déjà connus. La réponse contient instance serveur, génération, empreinte, nombre et les lignes, ou un accusé unchanged sans répéter les lignes. Le serveur valide au moins 669 hashes uniques, répond sans session de compte, rate-limit les demandes, sérialise les refresh concurrents et conserve le dernier snapshot valide si l’API devient momentanément indisponible. Une mutation F10, le timer de configuration et chaque restart peuvent publier une nouvelle génération.

Le client suit empty, requesting, ready ou degraded, avec les délais 250/500/1 000/2 000/4 000 ms. Resource start, sélection/spawn du personnage et redémarrage de la façade réamorcent le cycle sans créer plusieurs timers. Le tableau brut R1 reste accepté uniquement si aucun cache prêt n’existe ; un payload vide, incomplet, périmé, hors ordre ou un faux accusé unchanged ne remplace jamais le cache. Les diagnostics serveur et client exposent état, ready=<count>, schéma, empreinte, génération, dernière tentative/succès et erreur sans donnée personnelle. F8 journalise uniquement une transition vers ready=<count> ou l’épuisement des retries vers degraded, jamais chaque accusé inchangé. Aucun appel réseau ni log n’est émis par frame.

Tant que le catalogue n’est pas prêt, pompes et objets de dépannage s’arrêtent avant NUI ou réservation avec SYNC-FUEL-PORT en français ou anglais. Le pistolet ou câble exige une entité d’installation réelle ; les objets portables emploient un prop tenu en main sans inventer de pompe ni corde. Tous les props, cordes, freezes et focus sont nettoyés à la fermeture, à l’erreur et au onResourceStop.

La façade LC physique utilise le contrat v2 (physical=true, profil 1) et l’export client read-only medusaPhysicalProfile. Il fournit les props thermiques/électriques, l’animation de prise, le bone main 18905, les offsets/rotations, l’ancrage du flexible et la rotation véhicule issus de la copie pinée. Le runtime économique amont client_refuel.lua reste non chargé : il ne peut donc devenir ni writer, ni autorité de paiement, ni seconde machine de session.

Pour une installation fixe, l’automate serveur est reserved -> carrying -> attached -> delivering -> returning -> closed. Les intentions réseau rate-limitées medusa:energy:equipmentAction (taken, attached, detached, returned) portent la session et l’entité réseau du pistolet. Le serveur revalide source, personnage, routing bucket, propriétaire réseau, véhicule, modèle, clé, immobilité, moteur, installation, longueur de flexible et trappe avant chaque transition. Un tick et la projection medusaEnergyAttached sont strictement interdits avant attached ; les doublons et événements hors ordre ne peuvent avancer qu’une fois l’état canonique.

Le state bag versionné medusaEnergyEquipment contient uniquement les identifiants nécessaires à la représentation : session, propriétaire, véhicule, pistolet, bucket, installation, état et longueur de câble. Le pistolet est créé par le serveur avec CreateObjectNoOffset, conservé avec SetEntityOrphanMode(..., 2) et placé dans le routing bucket canonique avant que la session reserved soit envoyée au client. Le client charge le modèle LC exact, attend au plus 5 000 ms que le NetID serveur soit streamé et qu’OneSync lui en attribue naturellement le contrôle, puis attache l’objet au bone de main. Cette séquence ne dépend plus d’une création réseau cliente, incompatible avec certains modes de lockdown, ni d’une demande de contrôle bloquée par sv_filterRequestControl=4. Le serveur compare ensuite le NetID reçu à l’entité qu’il a créée et valide encore type, propriétaire et routing bucket ; un objet substitué est refusé. La suppression serveur est idempotente à la fermeture, au timeout, à la déconnexion et au restart.

Une intention concurrente pendant la validation ne republie jamais l’état intermédiaire reserved. Les observateurs du même bucket reconstruisent localement leur flexible entre l’installation GTA et cette entité, sur un timer de 750 ms, sans requête réseau par frame. Les autres buckets ignorent l’état.

Le catalogue tarifaire personnel alimente pricePerLiter en convertissant les centimes API en dollars LC. Avant chaque ouverture payante, le client vérifie la présence d’un prix numérique pour tous les produits activés sur le point. Une absence déclenche le bootstrap borné, ferme proprement l’interaction et affiche SYNC-ENERGY-PRICES ; un tableau absent n’est jamais transformé en tarifs zéro. Un prix explicitement configuré à zéro reste en revanche une valeur administrateur valide.

Le panneau complet conserve le focus uniquement jusqu’à la réservation. carrying rend le focus et laisse libres caméra et déplacement ; attached|delivering affiche seulement la progression LC transparente et sans focus. Le sélecteur body.medusa-progress-only .refuel-display-container et son équivalent .recharge-display-container partagent la marge clamp(18rem, 34vh, 28rem) ; la position du panneau Medusa et du panneau LC complet ne change pas. L’arrêt détache d’abord, ferme la session financière au volume confirmé, rembourse le reliquat puis conserve l’équipement en returning jusqu’à la même pompe. Le timeout de retour est de 60 secondes. Fermeture, désactivation, perte d’entité, dépassement de câble, mort, entrée en véhicule, déconnexion et restart passent tous par le cleanup idempotent des interactions, cordes, props, freezes, focus et state bags.

Consommation, state bags et garages

Le client calcule la consommation à partir du temps monotone, RPM, ralenti et facteur de classe ; le moteur coupé produit zéro. Le calcul n’émet aucun réseau ou log à chaque frame. Les snapshots persistants partent toutes les 20 secondes et aux frontières importantes. L’API compare identité, propriétaire et runtime_revision, borne le delta, met à jour le réservoir puis renvoie l’état canonique.

Le state bag medusaEnergy transporte propulsion, niveau, capacité, composition, facteur et révisions. Le state bag n’est pas une autorité de persistance. Les garages stockent et restaurent les mêmes champs, clôturent une session active avant rangement et conservent exactement 0 %.

Objets de dépannage

fuel_can_gasoline emploie energy_version=2, un produit thermique unique et 0–20 000 milliunités. Son poids est recalculé de 2 à 14 kg ; une metadata invalide vaut 14 kg et est rejetée à l’usage. Le produit ne change qu’à vide. L’achat plein coûte 300 $ ; le remplissage partiel suit le tarif Regular défini.

ev_charge_pack emploie 100 000 milliunités, pèse 8 kg, coûte 500 $ et est à usage unique. Il est accepté uniquement à ≤20 % sur EV et ajoute exactement 10 points. L’instance n’est supprimée que pendant le tick transactionnel qui confirme la totalité ; toute erreur antérieure laisse l’objet.

medusa:inventory:use résout aussi bien itemId (bouton NUI) que slot (raccourci 1–5), puis réémet la même instance complète vers medusa:inventory:useItem. Pour un portable plein, medusa-energy filtre tous les CVehicle sur metadata.energy_type, conserve le plus proche, ferme l’inventaire et prépare le prop tenu en main. Le client attend l’arrêt local du moteur puis 300 ms de publication OneSync avant medusa:energy:start; le serveur revalide toujours UUID, personnage, accès, compatibilité, distance, immobilité, moteur et intégrité. Le flux emploie directement target_mode=portable et payment_source=portable, sans ouvrir la façade de sélection.

Après acceptation, le chemin portable n’appelle jamais FreezeEntityPosition(..., true) et ne réapplique pas l’arrêt moteur dans sa boucle. equipment.lua surveille toutefois à chaque frame locale la trappe, la distance, le joueur, l’existence de l’entité, le moteur et le seuil de vitesse 0,55 m/s. Une invalidation émet au plus une fermeture, retire immédiatement le prop et rend canDeliver=false; la validation serveur du tick reste la dernière barrière contre une livraison en mouvement. Le helper de cleanup commun exécute toujours FreezeEntityPosition(vehicle, false) si l’entité existe, y compris après succès, refus, timeout, restart ou état gelé par une version antérieure. Le parcours station conserve, lui, son gel physique, son pistolet et sa corde.

MedusaEnergyEquipment.start prépare uniquement le prop et une ligne d’animation à l’état waiting. main.lua appelle startPortableAnimation après un événement medusa:energy:session réussi, portable et non terminal. La transition waiting -> loading -> playing est idempotente : les snapshots de progression ne rejouent ni l’orientation ni TaskPlayAnim. Les profils sont fuel_can_gasoline -> weapons@misc@jerrycan@/fire et ev_charge_pack -> mini@repair/fixing_a_ped, tous deux avec mouvement conservé. Le chargement du dictionnaire est borné à 3 000 ms et son échec reste purement visuel. La ligne enregistre le Ped, le dictionnaire et le clip réellement lancés ; cleanupRow exécute leur StopAnimTask exact et libère le dictionnaire avant le retrait du prop et la restauration ambiante.

Chaque objet portable porte aussi son propre modèle et son propre transform d’attache. Le bidon GTA w_am_jerrycan utilise l’os main 57005, l’offset (0.1800, 0.1300, -0.2400) et la rotation (-165.8693883, -11.2122753, -32.9453021) ; le pack électrique conserve son os 57005, son offset (0.12, 0.02, -0.02) et sa rotation (-85, 0, 15). Le helper unique attache le prop au préflight, puis une seule fois après TaskTurnPedToFaceCoord et avant TaskPlayAnim, afin de reprendre le Ped et l’os courants. Il n’est jamais appelé par la boucle frame et n’emploie aucune arme temporaire.

main.lua reconnaît une session par payment_source=portable, conserve l’UUID transmis par l’inventaire et clone uniquement son snapshot canonique vers l’événement local LC. Le ratio est calculé en points de base depuis initial_milliunits, target_milliunits et confirmed_milliunits; livré, restant et niveaux initial/cible sont dérivés des mêmes entiers. Le bridge rejette les révisions ou quantités confirmées en recul, accepte une progression sans panelOpen/currentPayload et ferme aussi lorsque seul progressOpen est vrai. Le final à 100 % est maintenu 650 ms uniquement pour ev_charge_pack; un nouveau snapshot, une fermeture ou un restart invalide son token. Les deux vues utilisent le rail DOM commun, la safe-zone R6 et aucun focus CEF.

Les achats prévalident poids et emplacement d’inventaire avant réservation. Session, paiement, instance et événement partagent le même identifiant logique afin qu’un retry ne crée ni double item ni double débit.

Le payload optionnel shopOnly=true de medusa:energy:openPoint est réservé à l’achat depuis une installation sans véhicule compatible. Le client sélectionne le véhicule compatible le plus proche avant de choisir ce mode ; le parcours normal garde donc toutes ses validations moteur, mouvement, clé et trappe. En boutique, le serveur reconstruit seededPoint ou physicalConnector, dérive lui-même fuel_can_gasoline ou ev_charge_pack, puis contrôle personnage/compte, flags, station active, routing bucket et distance. purchasePortable répète ces contrôles et refuse toute divergence entre l’objet demandé et le type d’installation avant d’appeler la transaction API existante.

La façade reçoit shopOnly et shopItemType, masque les contrôles de véhicule et ouvre seulement la confirmation de paiement adaptée. Ce chemin ne touche ni vehicleDescriptor, ni le catalogue des trappes, ni les maps de session/équipement. Un garde concurrent par source et la désactivation des boutons évitent un double achat pendant la requête ; le succès rend le focus et libère l’interaction, tandis qu’un échec réactive les choix.

Les erreurs HTTP nécessaires à l’interface empruntent l’export additif medusa-core.apiRequestWithRetryResult. Il renvoie une enveloppe Cfx sérialisable {ok,data|error} afin qu’un rejet asynchrone inter-ressource ne perde pas code, status et correlation_id. Les exports historiques qui lèvent une erreur restent inchangés. medusa-energy reconstruit localement l’erreur, traduit le code pour le joueur et écrit un diagnostic borné sans payload ni secret. Ainsi, capacité/poids, emplacement, fonds et panne API ne deviennent plus le fallback ambigu « service indisponible ».

Le callback Alt expose silent=true uniquement pour ray_miss, candidate_missing et not_a_vehicle. target_stale, mode inactif, fournisseur absent/en erreur, distance, action et panne CEF conservent le toast localisé et le code de corrélation.

Administration, permissions et audit

L’API commune alimente F10 et le web. Les six permissions restent identiques sur toutes les surfaces. Les seeds sont origin=seed, protected=true : déplacement, rotation, désactivation et restauration sont possibles ; le hard delete est refusé. Les customs peuvent être dupliqués ou supprimés après fermeture des sessions actives. Toute mise à jour vérifie expected_revision.

Les actions spatiales (GPS, TP, placement, calibration visuelle, test, restauration) restent dans F10. Pour une trappe, le client demande d’abord au résolveur commun son point monde/local effectif ; medusa-entity-placement initialise le marqueur à ce point, conserve le véhicule cible, désactive la caméra pendant l’ajustement, conserve le résultat en coordonnées locales et ne soumet qu’après E. Retour arrière, Échap, déplacement ou perte de la cible annulent sans mutation.

Le web gère recherche, filtres, prix, produits/débits, catalogue, activation, audit et corrections numériques de trappe. Il explique que la capture spatiale est réservée à F10. Lecture énergie : ADMIN_ALLOW_VIEW_ENERGY; création, édition et reset de calibration : ADMIN_ALLOW_MANAGE_ENERGY_CATALOG. Chaque mutation revalide modèle/hash, finitude, bornes, justification et expected_revision, puis conserve acteur, surface, avant/après et révision dans l’audit. Les erreurs web et FiveM affichent le message et Code <code> ; la préférence de langue permet les messages runtime FR/EN connus.

Rétention

Le scheduler et l’action administrative typée APPLIQUER appellent le même job :

  • après 90 jours, les événements techniques snapshot.*/preference.* et contextes de sessions sont rédigés ;
  • après 365 jours, les totaux par produit/propulsion sont sérialisés sans identifiant, hashés en SHA-256, consignés dans un événement agrégé, puis les lignes anciennes sont purgées ;
  • request_id rend chaque exécution idempotente et une exécution réussie dans les dernières 24 h évite un second job automatique.

Migration et exploitation

0040_vehicle_lc_fuel suit directement 0039_vehicle_energy_simple, possède un identifiant Alembic ≤32 caractères et refuse le downgrade. Elle conserve tous les pourcentages, crée les états de réservoir, remplace les six prix, réactive les deux objets, ajoute les modèles connus et seed 26+14 installations. Les UUIDs sont dérivés de clés stables. L’ancien historique terminal reste lisible et les 22 points électriques 0038 restent archivés.

L’ordre DDL/DML est intentionnel : la migration retire d’abord les contraintes CHECK remplacées, transforme ensuite toutes les données héritées de 0039 — notamment l’unique tarif gasoline/liter —, insère les prix et seeds définitifs, puis recrée les contraintes en dernier. PostgreSQL validant un CHECK dès sa création, avancer cette dernière étape avant la conversion rendrait le chemin réel 0039 -> 0040 impossible. Toute l’opération reste dans la transaction Alembic ; un échec restaure donc le schéma et les données de départ.

Le rollback est logique : désactiver les flags, laisser le recovery finaliser et livrer une migration corrective en avant. Ne jamais tenter de revenir à 0039 par downgrade.

0041_vehicle_fuel_ports suit directement 0040 et refuse également le downgrade. Son extraction est reproductible avec node fivem/scripts/extract-lc-fuel-ports.mjs --check; elle contrôle commit, nombre, hash du bloc, noms/hash uniques et Buffalo. Le harnais PostgreSQL 0041 rejoue les fixtures 0039/0040, vérifie que les tables et données 0040 restent byte-logiquement inchangées, les 669 seeds protégés, les triggers de protection, les invariants historiques 0040 et l’idempotence d’un second upgrade head.

0042_energy_charger_heading suit directement 0041, garde un identifiant compatible avec la colonne Alembic de production et refuse aussi le downgrade. Son harnais PostgreSQL isolé prépare une seed intacte, une seed tournée manuellement et une installation custom, applique 0042 puis head, compare les empreintes de tous les champs hors heading, exige l’audit unique et démarre /ready. Un retour d’orientation ultérieur doit prendre la forme d’une nouvelle migration conditionnelle : modifier 0040 ou downgrader écraserait potentiellement des choix F10 postérieurs.

Head courant : 0043_energy_station_anchors. Le harnais api/tests/postgres/run_vehicle_energy_0043.ps1 utilise des binaires PostgreSQL Windows fournis explicitement, un cluster neuf uniquement sur 127.0.0.1, sans service système ni base distante. Il conserve dump/logs, compare toutes les tables témoins, teste sessions/archives/éditions/couverture, rejoue puis refuse le downgrade, et arrête son propre cluster. Ne pas utiliser un volume de production. Les dumps de schéma avant/après doivent être identiques : 0043 n'a aucun DDL métier. Le contrôle global alembic check signale actuellement des écarts modèles/schéma déjà présents en 0042 (index, contraintes et nullabilité). Le harnais les conserve dans deux logs et exige l'absence de dérive nouvelle ; il ne les masque pas par une migration hors périmètre.

Flags :

VEHICLE_ENERGY_ENABLED
VEHICLE_ENERGY_CONSUMPTION_ENABLED
VEHICLE_ENERGY_STATIONS_ENABLED
VEHICLE_ENERGY_ELECTRIC_ENABLED
VEHICLE_ENERGY_PORTABLE_ENABLED
VEHICLE_ENERGY_INTERFACES_ENABLED
VEHICLE_ENERGY_GARAGE_ENABLED
VEHICLE_ENERGY_DETAILS_ENABLED

Les coupures sont fail-closed et contrôlées à la fois par l’API, le serveur FiveM et les entrées client. Le flag global refuse toute nouvelle opération. CONSUMPTION arrête les snapshots et la consommation Medusa ; STATIONS vide la publication des stations et retire props, blips et interactions ; ELECTRIC retire les points EV publiés et refuse sessions/pack EV ; PORTABLE refuse achat, remplissage et utilisation ; INTERFACES masque F5/F7/NUI et désactive les interactions joueur. DETAILS conserve la jauge F5 mais masque composition et télémétrie détaillée. Une session déjà durable reste clôturable/récupérable après une coupure afin de ne laisser ni lock ni paiement en attente.

Après un changement de flags, medusa-energy ferme explicitement chaque session devenue interdite au dernier montant confirmé (feature_disabled). L’arrêt de la façade lc_fuel déclenche la même barrière (facade_stop) pour toutes les sessions physiques, publie un catalogue désactivé et nettoie l’état client. Les motifs système ne sont émis que par les hooks serveur : l’événement de fermeture joueur normalise toujours le motif reçu en player_cancelled.

Réglages associés : snapshot 20 s, sync stations 30 s, flexible routier LC 7,5 m, timeout de réservation/prise 120 s, timeout de retour du pistolet 60 s, timeout session durable 120 s, recovery 15 s, rétention 90/365 jours et rate limit 30 actions par 10 s. Le timeout de retour ne s’applique jamais à un plein delivering encore actif. Le préflight de déploiement exige un backup, une copie PostgreSQL, les contrôles de writer, les flags initialement maîtrisés et la recette runtime complète avant activation globale.

Provenance tierce

  • LC Fuel : commit 405c376529a48c8aa25c1553ca7e7ee6469a5b8c, GPL-3.0 ;
  • LC Utils : commit 4a2d705830428ef71f1bd2127f8470eb52046fe5, GPL-3.0.

Chaque ressource contient LICENSE, THIRD_PARTY_NOTICES.md, PROVENANCE.json et PATCHES.md. Les dépendances browser sont vendored avec leurs notices MIT. Aucune récupération réseau runtime ou mise à jour flottante n’est autorisée.