Aller au contenu

Véhicules enregistrés : conception technique

Les garages rattachés délèguent leur autorisation au bâtiment et participent au relogement transactionnel de recovery. Voir Architecture technique de l'immobilier.

Architecture et autorité

PostgreSQL est l’autorité pour la propriété, la plaque, la génération des clés, le verrou et le cycle de vie. La ressource medusa-vehicles ne contacte jamais la base : elle utilise medusa-core.apiRequest, et résout le personnage actif via medusa-player-utils.

Les state bags medusaVehicleId, medusaPlate, medusaVehicleClass, medusaVehicleLocked, medusaVehicleLifecycle et medusaVehicleRevision synchronisent l’affichage et la corrélation. Ils ne prouvent jamais la possession d’une clé.

Données

La migration 0028_vehicle_keys ajoute :

  • registered_vehicles : identité, plaque unique, owner XOR, classe, génération, verrou, lifecycle, bucket, états initial/dernier/sûr, apparence, dégâts et révision runtime ;
  • vehicle_keys : véhicule, génération, statut, type et lien unique vers l’instance d’inventaire ;
  • vehicle_locksmiths : Ped, transform, bucket, prix, distance et état ;
  • vehicle_request_receipts : idempotence des enregistrements, transferts, serruriers et verrouillages ;
  • item_catalog_entries.system_managed et l’entrée protégée vehicle_key.

Les contraintes assurent un seul type de propriétaire, des générations positives et des états bornés. Les clés révoquées gardent leur historique mais perdent leur lien d’inventaire.

La table autorise volontairement plusieurs lignes vehicle_keys actives pour le même véhicule, la même génération et le même inventaire : chaque ligne correspond à une instance physique distincte. _valid_key() est donc un prédicat d'existence. Il sélectionne uniquement un identifiant avec une borne d'une ligne après avoir filtré véhicule, génération, statut, personnage, catégorie character et item vehicle_key. Il ne doit jamais employer une lecture exigeant une ligne unique sur ce jeu de résultats. Si item_instance_id est fourni, le prédicat ajoute ce filtre exact et ne se rabat pas sur un autre double.

API

Les routes internes /internal/v1/vehicles couvrent runtime, lock, moteur, snapshots, serrurier et administration F10. Les routes web /admin/vehicles et /admin/locksmiths réutilisent les mêmes services et les contrôles OAuth, permission et CSRF existants.

GET /admin/vehicles/owner-options exige ADMIN_ALLOW_EDIT_VEHICLE. Il accepte un type character|group et une recherche de 2 à 100 caractères, retourne au plus vingt objets minimaux {id, type, label, secondary_label} et ne transmet aucun identifiant Discord ou FiveM. La modale web conserve l'UUID uniquement comme valeur interne de l'option choisie. Les réponses asynchrones obsolètes sont ignorées et les recherches restantes sont annulées à la fermeture.

VehicleRead.owner_display_name est construit côté API par vehicle_owner_display depuis la relation personnage ou groupe. Les deux relations sont chargées avec selectinload dans la requête véhicule : le nombre de requêtes reste borné pour une liste entière et la NUI ne reçoit pas l'identité technique comme libellé. Le fallback est INCONNU.

F10 conserve cette réponse dans adminVehicleRows. vehicle-admin-utils.js construit le corpus de recherche uniquement depuis la plaque normalisée et owner_display_name; modèle, UUID et autres identifiants techniques ne sont pas des clés de recherche cachées. Le champ filtre ce cache sans callback réseau. personalVehiclesForCharacter exige l'égalité exacte de owner_character_id et l'absence de owner_group_id. Les listes globale et personnage appellent toutes deux l'unique createAdminVehicleCard, de sorte que permission, désactivation et callback d'une action ne puissent pas diverger entre les deux vues. Ces contrôles NUI ne remplacent jamais les permissions serveur des callbacks et routes existants.

Avant un transfert, le service vérifie l'existence du propriétaire personnage ou groupe et du personnage destinataire. Ces contrôles précèdent le verrouillage du véhicule et la révocation de l'ancienne génération : une sélection disparue ou une requête falsifiée échoue sans retirer les clés existantes.

Les six permissions administratives sont :

  • ADMIN_ALLOW_VIEW_VEHICLES ;
  • ADMIN_ALLOW_REGISTER_VEHICLE ;
  • ADMIN_ALLOW_EDIT_VEHICLE ;
  • ADMIN_ALLOW_MANAGE_VEHICLE_KEYS ;
  • ADMIN_ALLOW_DELETE_REGISTERED_VEHICLE ;
  • ADMIN_ALLOW_EDIT_LOCKSMITHS.

Le Founder les reçoit via ALL_PERMISSIONS. La suppression d’un personnage ou groupe propriétaire est bloquée tant que ses véhicules n’ont pas été transférés ou supprimés.

Transaction et concurrence

L’enregistrement verrouille l’inventaire destinataire, crée le dossier, l’item, le lien de clé et le reçu, puis effectue un seul commit métier. Le serrurier verrouille dans l’ordre inventaire, compte, véhicule/clés ; le débit, le ledger et la mutation de clé partagent le commit final. Un solde insuffisant est vérifié avant toute création/révocation.

Le runtime applique un cooldown d’une seconde, cinq requêtes par fenêtre de dix secondes et un mutex par véhicule. L’API rend le request_id de verrouillage idempotent. Les snapshots obsolètes sont ignorés grâce à observed_at.

Les parcours verrouillage, moteur, garage et serrurier consomment tous le même prédicat de possession. Le nombre de doubles ne change ni l'autorisation ni le coût de la requête : la lecture s'arrête à la première clé valide. Les limites et paiements du serrurier continuent de compter les lignes actives séparément.

La restauration F10 transmet un mode last_location ou front à la ressource véhicules. Elle vérifie le bucket, les occupants et sérialise les demandes par UUID. Pour front, le transform est calculé depuis le Ped serveur ; la NUI n'envoie jamais de coordonnées. L'API revalide ADMIN_ALLOW_EDIT_VEHICLE et le bucket, puis persiste le transform dans last_state et last_safe_state sans migration. Le runtime déplace l'entité canonique ou en crée une seule, réapplique l'état et diffuse medusa:vehicles:restored pour couper le moteur.

Les exports serveur locateRegistered et restoreRegistered ne confèrent aucune permission. medusa-admin impose ADMIN_ALLOW_VIEW_VEHICLES pour GPS, cette permission plus ADMIN_ALLOW_TPC pour SE TP, et ADMIN_ALLOW_EDIT_VEHICLE pour restaurer. Les audits vehicle.waypoint, vehicle.teleport et vehicle.restore enregistrent la cible et les coordonnées dérivées côté serveur.

Runtime FiveM

Au démarrage et toutes les 15 secondes, le serveur charge les dossiers active, conserve un registre UUID/réseau et utilise CreateVehicleServerSetter. Il ne crée pas de doublon si une entité valide est déjà associée. Plaque, verrou et lifecycle sont appliqués avant interaction.

À chaque demande, le serveur revalide : personnage actif, entité véhicule, network ID enregistré, routing bucket et distance. Le client ne fait qu’identifier une cible et appliquer les natives visuelles. Il ne décide jamais du droit. Un cache client non autoritatif, alimenté par l'événement apply et le state bag medusaVehicleId, permet de cibler le véhicule malgré un retard de réplication ; le serveur conserve toutes les validations. Une demande U sans réponse libère son verrou local après trois secondes et affiche une erreur, avec un identifiant local empêchant un ancien timeout d'annuler une réponse plus récente. Les valeurs du JSON runtime sont converties avec la primitive Lua tonumber. Le chemin objet issu de medusa:inventory:useItem peut traverser le focus de l'inventaire, tandis que le chemin clavier reste bloqué par toute NUI focalisée ; pause, mort, NUI serrurier et requête en cours restent bloquantes dans les deux cas.

Le geste local de clé ne se déclenche que dans medusa:vehicles:feedback, après validation de l’entité et de la distance. À pied, le helper charge pendant au plus 3 000 ms anim@mp_player_intmenu@key_fob@ / fob_click, joue 650 ms avec le flag 49 puis arrête exactement ce clip. Une génération locale rend les timeouts obsolètes inoffensifs et l’arrêt de ressource nettoie la tâche. Le retour medusa:vehicles:error, le timeout de requête et les actions depuis un siège n’entrent jamais dans ce helper ; les flashes et le klaxon historiques restent inchangés.

Le moteur reste forcé éteint tant qu'aucune demande n'est autorisée. L'entrée conducteur ne déclenche plus de requête. Le contrôle GTA 71 « Accélérer » — Z dans la configuration AZERTY courante — demande un démarrage lorsque le moteur est éteint ; il suit le remapping natif GTA. La commande interne medusa_vehicle_engine, publiée par RegisterKeyMapping sous G par défaut pour les claviers AZERTY, demande un démarrage ou un arrêt et reste remappable dans FiveM. Le client émet engineToggle avec request ID, network ID et état souhaité. Le serveur revalide personnage, bucket, proximité, entité enregistrée et siège conducteur, puis appelle /authorize-engine seulement pour un démarrage. engineResult est renvoyé pour le succès comme pour le refus. Le pending client est libéré sur résultat, sortie, changement de véhicule ou timeout de trois secondes, ce qui interdit tout blocage silencieux durable. Après une autorisation de démarrage, le client applique la native en mode immédiat et maintient une fenêtre bornée de 1,5 seconde pendant laquelle la boucle anti-auto-start réitère l'allumage au lieu de le couper. Cette fenêtre disparaît dès que le moteur tourne, à l'arrêt ou à la sortie du véhicule. Le nom interne du mapping reste stable afin de ne pas créer de raccourci fantôme. FiveM persiste les affectations personnelles : G est le défaut des nouveaux réglages, sans écraser un remap existant.

Un arrêt G n'ouvre le délai Accélérer qu'après un engineResult corrélé et réussi avec running=false. Le client mémorise alors, pour le network ID courant, l'échéance monotone GetGameTimer() + engineAccelerationRestartDelayMs. Le défaut de config.json est 60000 ms et une valeur négative est ramenée à zéro. Pendant ce délai, le contrôle GTA 71 reste neutralisé et un nouvel appui n'appelle pas requestEngine, n'émet aucun événement serveur et n'affiche aucun texte. La commande G ne consulte pas ce timer : son redémarrage volontaire reste immédiat et un succès running=true efface l'échéance. Sortie du poste conducteur, changement de véhicule/siège ou arrêt de la ressource nettoient aussi cet état client. Le délai n'est ni persistant ni autoritatif et ne se propage donc pas à un autre conducteur ; toute demande réellement émise reste validée par le serveur.

La sauvegarde de position est envoyée toutes les 20 secondes depuis le véhicule observé, puis revalidée côté serveur. Une position sûre terrestre exige un véhicule sur ses roues, presque immobile et hors de l’eau ; les bateaux et aéronefs utilisent leurs règles propres. La restauration utilise d’abord last_safe_state, puis le dernier état et enfin l’état initial.

La NUI serrurier maintient html et body transparents et non interactifs. Le thème sombre et les événements de pointeur sont limités au panneau visible ; la règle [hidden] force sa disparition et la fermeture retire aussi l’état et le focus résiduels. Le panneau utilise un gradient et une ombre strictement locaux, sans backdrop-filter, car ce filtre alloue dans CEF une surface de composition noire plus grande que la carte. Le monde reste donc visible en dehors des bords du panneau ouvert. Le document ne déclare pas non plus color-scheme : cette propriété peut rendre le canvas CEF plein écran opaque malgré background:transparent. setPanelVisible est l'unique transition HTML pour le chargement initial, le serrurier, les garages, Alt/F5 et les fermetures ; closeUi conserve le nettoyage Lua de SetNuiFocus, SetNuiFocusKeepInput et du mode cible. Le test fivem/tests/nui-transparency.test.mjs découvre toutes les ui_page Medusa, refuse les racines opaques et color-scheme, et renforce spécialement le contrat #panel[hidden] de cette ressource. Seul le loading screen volontairement opaque est exempté.

Le sélecteur NUI consomme exclusivement LocksmithOptions.accounts : comptes personnels actifs dont l'adhésion du personnage est acceptée. Il affiche numéro et solde, ou une option désactivée « Aucun compte personnel actif » accompagnée du parcours bancaire. Les actions restent désactivées sans compte ; le serveur conserve dans tous les cas sa validation autoritative au moment du paiement.

La synchronisation des serruriers indexe les Peds par UUID et conserve une empreinte du modèle, du nom, du transform, de la distance et de l'offset. Une empreinte modifiée désenregistre l'ancienne interaction avant de recréer le Ped. Le transform issu du placement contient l'origine de l'entité Ped ; locksmithZOffset=1.0 est soustrait par medusa-interactions pour replacer ses pieds au sol. Un modèle impossible à charger ou un transform invalide produit une ligne [medusa-vehicles] dans la console client et sera retenté lors de la synchronisation suivante, sans enregistrer une fausse instance locale.

Configuration et rollback

Variables API :

VEHICLES_ENABLED=true
VEHICLE_MAX_ACTIVE_KEYS=10
VEHICLE_LOCK_DISTANCE=15
VEHICLE_FEEDBACK_DISTANCE=20
VEHICLE_SNAPSHOT_SECONDS=20

Le défaut du dépôt est actif pour l'auto-déploiement de VirgiilBranch. Une valeur Portainer explicite reste prioritaire. Le fichier medusa-vehicles/config.json porte les cadences et limites runtime correspondantes, dont engineAccelerationRestartDelayMs=60000. La ressource est démarrée après inventaire/interactions/banque et avant medusa-admin.

Pour revenir en arrière, désactiver le flag et revenir au code antérieur sans downgrader la migration après création de données réelles. Les tables et clés sont conservées pour une correction en avant.

Extension garages et plaques

La migration 0032_vehicle_garages ajoute vehicle_garages, vehicle_garage_spawns, vehicle_garage_reservations et vehicle_plate_history, étend registered_vehicles avec le style, le carburant, les garages courant/dernier et le motif de fourrière, puis migre atomiquement les plaques et les libellés de clés. Elle insère neuf garages actifs et trois sorties par site.

vehicle_plates.py centralise la validation 1–8 ASCII, la génération signée et les lots ambiants. vehicle_garages.py porte l'autorisation propriétaire/groupe+clé, le rangement, la réservation TTL, la finalisation, la compatibilité terre/mer/air, la fourrière et la suppression conservatrice. Les routes internes restent sous /internal/v1/vehicles; le web utilise /admin/vehicle-garages. PostgreSQL reste autoritaire et les entités réseau ne sont créées ou supprimées qu'après les transitions API correspondantes.

medusa-vehicles synchronise garages et plaques ambiantes, crée les blips/interactions et dessine à courte portée un cercle bleu de 2 mètres sans prop persistant. Une seule interaction contextuelle à 1,25 mètre ouvre la NUI à pied ou range immédiatement le véhicule du conducteur. Le serveur impose la proximité et le routing bucket, puis sérialise les opérations par véhicule. Le reconcile ne reçoit que les dossiers active; les véhicules garés ou en fourrière ne sont donc jamais recréés.

Le bootstrap client est un contrat bloquant : medusa-vehicles/client.lua porte à la fois les exports clients, les touches U/G, les serruriers, les state bags, les cercles et les blips. Une erreur de syntaxe y rend donc tout le module client indisponible. npm --prefix fivem test analyse les Lua des ressources [local] avec luaparse; le harness neutralise uniquement les littéraux de hash CitizenFX `model_name` avant l'analyse Lua 5.3. medusa-admin dépend toujours explicitement de medusa-vehicles, vérifie son état et protège describeTarget par pcall afin de terminer le callback NUI avec une erreur française plutôt qu'une promesse rejetée si la ressource est absente.

À chaque garageSync, le client supprime d'abord l'interaction et le blip connus pour l'UUID, puis recrée la version autoritative ; le cercle n'est qu'un marqueur dessiné à proximité. Les garages absents de la liste runtime — désactivés ou supprimés — sont nettoyés, comme tous les artefacts locaux lors de onResourceStop.

Le TP garage F10 suit adminGarageTeleport → medusa:admin:garages:teleport → /internal/v1/vehicles/admin/garages/list. La NUI envoie uniquement l'UUID. Le serveur exige ADMIN_ALLOW_VIEW_GARAGES et ADMIN_ALLOW_TPC, retrouve la ligne API, vérifie les coordonnées et le routing bucket, calcule une position à 2,5 m de la borne puis réutilise medusa:admin:teleportToCoordinates. medusa:admin:tpcResult conditionne l'audit vehicle.garage.teleport au succès réellement confirmé.

Les mutations spatiales sont séparées de l'édition des métadonnées : PUT .../{garage_id}/anchor déplace uniquement le rond, tandis que POST/PUT/DELETE .../{garage_id}/spawns/{spawn_id} gère une sortie ciblée. La révision du garage protège les écritures concurrentes ; l'UUID d'une sortie ne change pas lors d'un déplacement. Une réservation active et la dernière sortie active d'un garage activé sont protégées. Un nouveau garage est toujours créé inactif et sans sortie implicite.

F10 obtient les suggestions via POST /internal/v1/vehicles/admin/owner-options, avec permission, recherche et pagination bornée. medusa:admin:message transporte kind, code, scope et un message affichable ; la NUI rend ce résultat dans le panneau, sans retirer les traces F8/F9.

Configuration : VEHICLE_IMPOUND_FEE, VEHICLE_GARAGE_RESERVATION_TTL_SECONDS et VEHICLE_AMBIENT_PLATE_BATCH_SIZE.

Handling ORIGINAL et profils

fivem/tools/handling-catalog.mjs parcourt les ressources dans l'ordre de chargement et génère medusa-vehicles/data/handling-original.json. Chaque entrée contient modèle, handling name, famille, ressource, chemin, valeurs whitelistées et empreinte. Le catalogue actuel couvre les fichiers GTA/DLC observés au runtime, 244 entrées réalistes et six ressources custom ; une source dupliquée devient ambiguous et une source invalide ne reçoit aucune surcharge relative.

L'API vehicle_features persiste sources, profils, versions et affectations. La résolution est déterministe : catégorie, puis modèle, puis UUID, avec une seule sortie effective. Les valeurs relatives sont recalculées depuis la baseline observée, jamais depuis le résultat précédent. Le client mémorise les valeurs natives avant première application Medusa et restaure exactement ces valeurs pour ORIGINAL ou lors d'une désactivation. Les profils absolus sont limités à une whitelist typée et ne peuvent pas être affectés à une catégorie entière.

HandlingAssignmentRead conserve target_key et vehicle_id pour les mutations autorisées, mais ajoute target_display_name, vehicle_model, vehicle_plate, owner_display_name et owner_type pour l'affichage. list_assignments charge en lots les profils puis les véhicules avec leurs seules relations propriétaire utiles : le nombre de requêtes reste constant pour une page et aucune carte ne déclenche de N+1. Les adaptateurs F10/web construisent cartes, recherche et confirmation depuis cette projection. Pour ORIGINAL, assignment_count compte seulement les lignes explicites ; le fallback automatique reste porté par resolve_handling sans créer une ligne par véhicule.

Les deux adaptateurs appliquent la même hiérarchie de lecture : Profils disponibles, Profils appliqués puis Base ORIGINAL des véhicules. La carte de source ne rend par défaut que modèle, famille traduite, provenance normalisée et état français. Le composant natif details/summary expose de façon accessible et copiable les champs techniques complets (handling_name, ressource, chemin, empreinte, détection, révision et diagnostic). Les valeurs humaines et techniques alimentent le même index de recherche ; un diagnostic missing, invalid ou ambiguous reste visible sans ouvrir le détail.

Le runtime télécharge le catalogue par lots bornés, applique uniquement les entités streamées et publie progression/diagnostics. Les previews possèdent un UUID et une expiration serveur ; le client restaure la résolution persistante sur annulation, timeout, disparition, arrêt de ressource ou changement de cible. Aucun payload NUI joueur ne contient profil ou paramètres handling.

Compartiments et cycle de vie

Les compartiments enregistrés sont créés paresseusement dans vehicle_compartments, un par type et par UUID. Leur inventaire conserve les contraintes communes et un état active, stored, unavailable ou purged. Les priorités de capacité sont classe < modèle < UUID. La boîte à gants est commune à conducteur/passager avant et fixée à 5 slots/5 kg par défaut ; le coffre suit la grille GTA centralisée dans vehicle_compartments.py.

Une cible PNJ est identifiée par boot_token + network_id + model + vehicle_class. Le serveur FiveM recalcule modèle et classe à partir de l'entité ; le client ne peut donc pas transformer un véhicule incompatible en coffre. La conversion vers un véhicule enregistré verrouille source et destination, vérifie poids/slots, déplace toutes les instances et supprime l'identité temporaire dans le même commit. Toute exception laisse l'enregistrement et le contenu temporaire intacts.

Le résolveur de contexte medusa-vehicles possède l'ID stable medusa-vehicles et la priorité 100. Son inscription est idempotente, vérifie le booléen retourné par medusa-inventory, réessaie un nombre borné de fois si la dépendance n'est pas encore démarrée, se réinscrit après son restart et s'efface au stop. L'ambiguïté de deux coffres renvoie handled=true avec un message : l'appui TAB est consommé et ne peut donc pas ouvrir simultanément l'inventaire personnel.

Le moniteur client capture l’instance activeCompartment avant son Wait(250). Après ce yield, il compare l’identité du snapshot à l’état courant, lit exclusivement le snapshot stable et ne demande une fermeture que si cette même instance est encore active. Une fermeture vers nil ou l’ouverture d’un autre coffre/boîte à gants pendant l’attente abandonne donc l’itération obsolète sans déréférencement, sans fermer le nouveau contexte et sans tuer la boucle de surveillance.

publishVehicleRuntimeCapabilities constitue le contrat unique de readiness runtime : il republie les flags et résout medusaVehicleCompartments avec une promesse dédupliquée par network ID. Une sortie de garage attend ce contrat avant /garages/finalize; un échec supprime l'entité et utilise l'annulation de réservation existante. spawnRecord, l'enregistrement et la restauration attendent également cette publication avant leurs événements joueur, sans annuler une mutation API déjà commitée si une réparation ultérieure reste possible.

medusa:vehicles:featureStateRequest accepte l'ancien network ID ou le contexte enrichi du client, revalide entité, type, routing bucket, distance, hash de modèle et classe, puis republie flags et capacités. Le serveur conserve son rate limit par joueur ; le client déduplique cinq secondes par network ID. Les trains, véhicules spéciaux, cibles forgées et buckets étrangers ne peuvent pas obtenir de capacité.

Garage, fourrière et transfert de propriétaire gardent les mêmes UUID d'inventaire. Une suppression administrative non vide exige reject, transfer ou destroy, un request_id, un motif et, pour la destruction, plaque + seconde confirmation. Une explosion est commit-first : seul explosionEvent corrélé à l'entité appelle l'API idempotente ; aucune file locale ne rejoue une purge après panne.

Alt, F5 et sièges

medusa-interactions expose un fournisseur de cible générique activé seulement pendant Alt gauche. medusa-vehicles inscrit ce fournisseur sous pcall, vérifie le booléen retourné, signale un échec au joueur et recommence après le démarrage ou le redémarrage de medusa-interactions. Il revalide ensuite entité, distance, siège, verrou, clé, classe et rate limit avant de diffuser une action. Les portes/vitres sont dérivées des natives réellement valides ; le client ne répare jamais un élément endommagé. Les états non persistants (ouvrants, vitres, feux) sont répliqués aux clients présents, tandis que seuls les dégâts réels restent dans le snapshot 20 s.

Le fournisseur accepte une entité véhicule ou remonte du Ped raycasté vers le véhicule qu'il occupe. Il rejette explicitement l'absence de cible, le joueur déjà assis, les trains, les véhicules spéciaux et le flag désactivé. Le serveur publie les flags autoritatifs dans GlobalState.medusaVehicleFeatureFlags et dans le state bag de chaque entité. Si ces données ne sont pas encore visibles, le client demande une republication bornée ; le serveur revalide network ID, routing bucket, distance et type avant d'écrire le state bag. Cette tolérance de réplication n'autorise jamais une action lorsque le flag serveur est faux.

Le clic cible transporte un code corrélé, revalide le même véhicule, ouvre le panneau exactement une fois et rend un acquittement de transfert de focus. Une disparition, un éloignement, un refus du fournisseur, une erreur d'action ou une panne CEF produit un message visible avec ce code. Les logs debug sont limités aux changements d'état afin de rester exploitables sans spam par frame.

F5 garde SetNuiFocusKeepInput(true) afin de laisser conduire et sortir, mais une whitelist inversée neutralise chaque frame les axes souris, caméras véhicule/cinématique, roues radio/arme et attaques dans les groupes d'entrée 0 à 2. Les contrôles de direction, accélération, freinage, frein à main et sortie ne figurent jamais dans cette liste. closeUi est l'unique restauration du focus, du curseur et de KeepInput pour les panneaux véhicules.

Les changements de siège utilisent une réservation serveur par networkId:seat, un token opaque et un TTL de 5 s par défaut. Après acceptation, le client vérifie encore que la cible n'est pas occupée, annule les tâches GTA en cours puis place le Ped avec SetPedIntoVehicle sur l'index exact ; il n'éjecte jamais un occupant tardif. Le serveur ne confirme le succès que si GetPedInVehicleSeat(entity, targetSeat) === ped. Le flag Ped 184 empêche le déplacement automatique d'un passager vers le conducteur ; sa valeur antérieure est conservée puis restaurée sur conducteur, sortie, mort, changement/despawn d'entité, timeout, refus ou arrêt de ressource. Le moteur F5 appelle le même événement que G et ne contourne ni la clé physique ni le délai Accélérer de 60 s.

Feature flags

  • VEHICLE_HANDLING_ENABLED : coupe résolution, preview et surcharge, puis laisse ORIGINAL ;
  • VEHICLE_INVENTORIES_ENABLED : coupe ouvertures/mutations et restaure le fallback TAB historique ;
  • VEHICLE_CONTROL_MENUS_ENABLED : coupe Alt/F5 et sièges sans modifier U, G, garages ou serrurier.
  • VEHICLE_SPEEDOMETER_ENABLED : coupe collecte, rendu et page de personnalisation sans supprimer la préférence.

L'endpoint interne /internal/v1/vehicles/features expose leur état et leurs limites. Le serveur FiveM diffuse ce statut par state bag avant de rendre une surface, de démarrer un polling ou de faire une écriture. Les 16 combinaisons API des quatre flags sont indépendantes ; la recette runtime couvre séparément les quatre combinaisons compteur/F5. Les données restent en base quand un flag est éteint.

Préférence de compteur et télémétrie

vehicle_speedometer_preferences porte une ligne par player_accounts.id avec cascade de suppression, thème/éclairage allowlistés, scale_percent entier entre 60 et 140 par pas de 5, position_x|position_y finies dans [0,1], visibilité et révision optimiste. La migration 0036_speedometer_placement ajoute les trois champs, transforme small/normal/large en 80/100/120 et initialise (0.5,0.8). La colonne size reste seulement pour la compatibilité avec les applications et migrations publiées 0034/0035. Les valeurs par défaut v3 sont classic/100/0.5/0.8/true/auto, révision 1.

vehicle_request_receipts.actor_account_id rattache les PATCH idempotents au compte ; le fingerprint inclut thème, échelle, deux coordonnées, visibilité, éclairage et révision attendue. Le même UUID et la même empreinte rejouent la réponse canonique, tandis qu'un autre compte ou payload reçoit un conflit. La cible est toujours dérivée de medusa-player-utils.findBySource(source, true) : aucun UUID fourni par la NUI ne choisit le compte.

Contrats internes protégés par X-Internal-Api-Key :

  • GET /internal/v1/vehicles/accounts/{account_id}/speedometer-preference crée ou lit la ligne, même lorsque le flag compteur est coupé ;
  • PATCH /internal/v1/vehicles/accounts/{account_id}/speedometer-preference exige un UUID de requête, la révision attendue et le flag actif ;
  • GET /internal/v1/vehicles/features expose les quatre flags.

Le serveur FiveM transforme la session en appel API et répond uniquement au source par medusa:vehicles:speedometer:state|result. L'adaptateur serveur est la seule frontière snake_case/camelCase. Le client utilise classic/100/0.5/0.8/visible/auto en repli, enchaîne les retries 1/2/4/8/16 s et applique l'autorité récupérée sans restart. Une seule mutation est en vol ; une réponse n'est appliquée que si requestId, révision attendue et signature complète du brouillon correspondent encore. Une réponse tardive est ignorée puis l'état canonique est rechargé.

client/vehicle_telemetry.lua lit les natives localement à 20 Hz maximum, met en cache les champs lents 250 ms, omet NaN/infinis/capacités absentes et déduplique les snapshots. Le contrat v3 ajoute l'échelle et la position au contrat v2, conserve l'heure de jeu, le mode d'éclairage résolu et les champs optionnels turbo/dérive/surrégime seulement lorsqu'ils sont prouvés. L'événement local versionné medusa:hud:vehicleSpeedometer ne contient aucun identifiant. Il ne publie en permanence que pour le siège conducteur ; le preview passager est synthétique (000, N).

Les cartes F5 sont rendues par web/speedometer-art.js sous forme d'images transparentes depuis web/assets/speedometer/previews/{classic,d6_street,d7_racing}_{day,night}.png. Ces six images sont des exports lossless reproductibles des YTD et compositions Lua exacts ; leur manifeste contient les SHA-256 et l'outil d'export refuse toute source amont altérée. app.js choisit seulement le thème et le mode d'éclairage du brouillon. Le HUD réel reste le renderer natif de medusa-hud : aucune géométrie NUI parallèle n'est utilisée en conduite. Les notices tierces et la licence BSD 2-Clause sont distribuées avec les deux ressources.

Éditeur de placement du compteur

speedometerPlacementAvailability autorise l'entrée uniquement si le flag compteur et medusa-hud sont actifs, si le Ped vivant est conducteur du véhicule F5 courant, hors pause et à 1 km/h ou moins. Le snapshot appliqué reste distinct du brouillon. Le callback local speedometerPlacementUpdate consulte l'export HUD resolveSpeedometerGeometry, met à jour l'aperçu local et ne déclenche aucun événement réseau. Seul speedometerApply envoie le PATCH.

La NUI regroupe les mouvements dans requestAnimationFrame. Souris, sliders et clavier travaillent en coordonnées normalisées ; la manette est lue côté Lua avec GetDisabledControlNormal. Le masque SPEEDOMETER_PLACEMENT_CONTROLS complète le masque F5 normal uniquement pendant l'éditeur afin de neutraliser mouvement, accélérateur, frein, sortie, caméra et radio sans régresser la conduite avec le menu F5 normal. La fermeture unique rend SetNuiFocusKeepInput et le focus NUI, supprime l'aperçu et abandonne le brouillon.

Le renderer HUD reste l'unique propriétaire de l'enveloppe effective et renvoie position désirée, position rendue, bornes et zone sûre. Un recalcul à chaud peut déplacer la position rendue, jamais la position persistée et jamais PostgreSQL. Le régulateur a été supprimé de toutes les surfaces actives : aucun fichier client, mapping, callback NUI, flag, state bag ou paramètre ne peut injecter les contrôles longitudinaux.

Pont énergie

medusa-vehicles conserve identité, clés, moteur, lifecycle et snapshots ; medusa-energy ajoute le domaine énergie sans devenir propriétaire du véhicule. Le contrat state bag/API, la règle 1 % et la persistance 0038 sont décrits dans Énergie des véhicules.