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-energyvalide le runtime FiveM, orchestre les intentions et publie les state bags ;medusa-vehiclespossède l’identité, le cycle de vie, le niveau canonique en pourcentage et les règles moteur/garage ;medusa-bankingpossède les réservations, débits et remboursements ;medusa-inventorypossède les instances de bidon et de pack ;lc_fuelfournit NUI, composition visuelle et assets streamés ;lc_utilsfournit 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_id1: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_idoucontext_json.station_id, pour les cinq états actifs, y comprisrecovery_pending; pas de colonnestation_idsur 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.pyconserve les mêmes centres/noms pour la restauration explicite des seuls nouveaux seeds routiers ;point_seed_defaultsrenvoieNonepour 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 :
- anomalie de migration non résolue : blocage ;
- override véhicule actif ;
- état de réservoir persistant ;
- entrée catalogue modèle ;
- liste EV ou Diesel pinée ;
- entrée catalogue catégorie ;
- 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_idrend 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.