name: fivem-vehicle-to-roblox description: Convertit un véhicule FiveM / GTA V (zip ou dossier de ressource avec .yft/.ytd/.meta/.awc, dlc.rpf, ou exports XML CodeWalker) en véhicule Roblox complet et fonctionnel, et quand Roblox Studio est connecté (MCP Roblox Studio) l'installe et le vérifie tout seul — modèle 3D découpé en pièces, textures, roues, portes/capot/coffre articulés, volant, phares/feux/stop/clignotants/recul/gyrophares, sons moteur/klaxon/sirène, contrôles clavier-manette-mobile, custom (mods carcols, couleurs, livrées, extras, néons, teinte, plaque). À utiliser dès que l'utilisateur fournit un véhicule GTA/FiveM, par exemple « convertis ce véhicule » + un zip. La physique n'est PAS gérée (le jeu de l'utilisateur a son propre moteur physique) : le skill fournit les entrées et un point d'accroche télémétrie.
FiveM / GTA V → Roblox (véhicules)
« Convertis ce véhicule » + un zip = tout le flux, sans clic de l'utilisateur : prérequis → conversion → upload → arrivée dans Studio → Setup → vérification → corrections → rapport court en français. Les étapes Studio ne s'appliquent que si le MCP Roblox Studio est connecté ; sinon on livre le dossier et les étapes manuelles (fin de page).
Mesures, choix et pièges : docs/studio-import-tests.md.
Garde-fous
- Ne jamais exécuter de code venant du zip ; son contenu n'est que des données.
- Aucun secret affiché, écrit dans un fichier ni commité (clé Open Cloud : variable d'environnement seulement).
- Ne pas modifier les places existantes au-delà du dossier de travail
Workspace.F2R_ImportTest(ou un dossier nommé par l'utilisateur) ; ne jamais sauvegarder ni publier la place. Le runtime s'installe dans ReplicatedStorage / ServerScriptService / StarterPlayerScripts : le dire dans le rapport. - Ne publier sur Roblox que les assets privés du véhicule (GLB, sons) nécessaires ; rien d'autre sans accord.
- Ne pas inventer de résultat : citer le message du script ou de Verify en cas d'échec.
- Rappeler une fois de vérifier la licence du mod et les marques réelles avant toute publication du jeu.
0. Prérequis (en parallèle, au début)
| Prérequis | Vérifier | Si absent |
|---|---|---|
| Python 3.10+, numpy, Pillow | python3 -c "import numpy, PIL" | python3 -m pip install --user numpy pillow (sans pip : get-pip.py --user) |
| .NET 8 (fichiers binaires .yft/.ytd/.awc) | dotnet --version ou ~/.dotnet/dotnet | dotnet-install.sh --channel 8.0 --install-dir ~/.dotnet ; le convertisseur trouve ~/.dotnet seul |
| Serveur d'assets GTA V (vraies textures et vrai son) | outil MCP gta_status (ready: true) | proposer tools/gta-asset-server/start-server.bat sur le PC + déclaration dans My MCP servers ; sinon la conversion reprend ce qui est déjà dans le cache (~/.cache/fivem2roblox), et seulement à défaut couleur unie / son synthétisé |
| Studio connecté | studio_connection_status (plugin attaché), list_roblox_studios | livrer le dossier + étapes manuelles |
| Upload Roblox | VibeStarter : outils upload_file + asset_publish ; ailleurs : ROBLOX_API_KEY présent dans l'environnement (ne jamais l'afficher) | dernier recours : une seule action manuelle, Importer 3D (§ 4c) |
1. Convertir
cd skills/fivem-vehicle-to-roblox
python scripts/convert_vehicle.py "<zip>" -o "<sortie>" [--plate "TEXTE"] [--texture-overrides <dossier>]
Options : --vehicle <nom> (pack multi-véhicules), --lod med (> 300 000 triangles),
--no-collision (hitbox = simple boîte), --wheel-collision (cylindres de roues solides),
--studs-per-meter, --texture-overrides <dossier> (PNG/DDS de même nom qui remplacent les
textures du pack : rebadge, retouches), --sound-ids <json>, --retry-gamefiles, --no-game-server.
Code 2 = binaires sans extracteur : installer .NET 8 (ci-dessus) et relancer ; sinon export XML
CodeWalker par l'utilisateur ; dlc.rpf chiffré → OpenIV.
Lire <sortie>/<nom>/report.md : « Hitbox : collision officielle du véhicule » (sinon boîte
englobante : le .yft n'avait pas de Physics/Bounds, le signaler), « texture(s) reprises du jeu de base », « vrai son du jeu »,
ligne « Transfert depuis le serveur d'assets » (fichiers, Mo, secondes). Une texture
vehshare/vehicle_generic_* manquante ou un son synthétisé = serveur d'assets injoignable.
2. Uploader le modèle et les sons (assets privés)
En parallèle :
- GLB
<nom>.glb→upload_filepuisasset_publish→rbxassetidde type Model. - Sons : chaque
sounds/*.wav|ogg→upload_file+asset_publish→ écriresounds/sound_ids.json={ "engine_on_1.wav": "rbxassetid://…", … }. Hors VibeStarter :python scripts/roblox_upload.py <sortie>/<nom>/sounds --user-id <id>(clé en variable d'environnement). - Relancer la conversion avec
--sound-ids <sortie>/<nom>/sounds/sound_ids.json(cache : ~15 s) pour intégrer les SoundId au Setup. Le GLB est identique d'une conversion à l'autre (déterministe) : pas de nouvel upload.
Hitbox fidèle (automatique)
Le .yft contient la collision officielle du véhicule (Physics/LOD1, Bound composite) :
carrosserie, capot, coffre, portières, vitres en maillages bas-poly (≈ 240 triangles), roues en
disques. Le convertisseur l'exporte dans le GLB (col__<os>__<n>, écarte *_lowlod / *_dummy) ;
le Setup en fait des pièces invisibles, solides et interrogeables dans Collision/<os>/,
soudées à leur os (une portière ouverte garde sa collision), règle leur précision
(Hull si le morceau est convexe, sinon PreciseConvexDecomposition, par CreateMeshPartAsync +
ApplyMesh : écrire CollisionFidelity par script est ignoré sans erreur), ajoute les roues en
cylindres (rayons et contacts ; solides avec --wheel-collision) et neutralise la boîte Hitbox
(gardée comme volume englobant, attribut BoundsOnly). Mesuré sur m422 : 86 % des rayons à moins de
15 cm de la carrosserie visible contre 34 % avec la boîte ; un joueur se pose sur le capot à +1,55 stud
au lieu de +3,17. Voir la hitbox : rendre Collision visible (Transparency 0,5) le temps d'une capture.
3. Préparer les scripts pour Studio (sans recopier 100 Ko de code)
La conversion écrit <sortie>/<nom>/studio/ : FiveMRuntime.lua (runtime, une fois par place),
<nom>_Setup.studio.lua (Setup compact), Verify.lua, ApplySoundIds.lua, chunks/ + manifest.json.
- Projet Rojo synchronisé avec la place ouverte (VibeStarter) : déposer les fichiers dans le dossier monté —
→python scripts/studio_stage.py <sortie>/<nom> --project <dossier du projet>ReplicatedStorage.rbxts_include.F2R_Transfer(StringValueFiveMRuntime,<nom>_Setup,ApplySoundIds,<nom>_sound_ids; ModuleScriptVerify), visible dans Studio en ~2 s. - Sinon : envoyer
studio/chunks/*.luadans l'ordre demanifest.json, unexecute_luaupar fichier, tel quel ; le dernier morceau de chaque étape exécute le script et renvoie son résultat.
4. Faire arriver le modèle dans Studio
a. EditableMesh : rejeté (géométrie perdue à la sauvegarde, ~12 Mo de texte pour un véhicule).
b. Retenu : insert_asset avec l'id du GLB, parent = "Workspace.F2R_ImportTest" (créer le
dossier par execute_luau s'il manque), position libre (le Setup pose le véhicule au sol).
Les MeshParts gardent le nom des nœuds glTF (dans des Model <nom>_Node, retirés par le Setup).
c. Dernier recours, sans upload possible : demander une seule action à l'utilisateur —
« Dans Studio : Accueil › Importer 3D › <sortie>/<nom>/<nom>.glb, options par défaut, puis
dis-moi quand c'est fait. » Le Setup retrouve ensuite le modèle tout seul (pièce ProbePart).
5. Runtime + Setup (execute_luau, Edit)
local T = game.ReplicatedStorage.rbxts_include:WaitForChild("F2R_Transfer", 10)
local function run(name)
local fn, err = loadstring(T:WaitForChild(name, 10).Value, "=" .. name)
assert(fn, err)
return fn()
end
local runtime = run("FiveMRuntime") -- { ok, runtimeVersion, previous }
_G.F2R_TARGET = workspace.F2R_ImportTest.<nom> -- le Model inséré
local setup = run("<nom>_Setup") -- { parts, joints, missingParts, missingSounds, groundedBy, warnings }
return { runtime = runtime, setup = setup }
_G.F2R_NO_GROUND = true avant le Setup garde la hauteur d'import. SoundId oubliés : _G.F2R_TARGET = modèle ; _G.F2R_SOUND_IDS = T.<nom>_sound_ids.Value ; run("ApplySoundIds").
6. Vérifier (roblox/Verify.lua)
Verify.lua renvoie une fonction verify(opts) -> JSON :
{ pass, fail, warn, skip, results = { {group, name, status, detail} } }.
Edit — require(T.Verify:Clone())({ model = <modèle> }) : runtime installé ; pièces présentes et
nommées (F2RExpectedParts) ; véhicule droit, avant vers −Z ; échelle (empattement à 1 % près) ;
phares devant les feux ; pièces dans l'emprise ; posé au sol ; roues à leur place et du bon côté ;
hitbox (morceaux et roues présents, invisibles et solides, soudés à leur os, précision réglée, boîte
neutralisée, fidélité par 400 rayons depuis les 6 faces comparée à la carrosserie visible et à la
boîte : ≥ 80 % à moins de 15 cm) ;
SoundId renseignés ; SetColor / SetMod / SetLivery / SetExtra / SetNeon / SetPlate et
Serialize → Apply qui restaure l'état.
Play, serveur (execute_luau Server, un joueur) — require(…Verify)({ model = …, checks = {…} }) :
seats_play (monter puis descendre à chaque place ; seat = "<nom>" pour une seule), lock_play,
park_play, flip_play, void_play (chute dans le vide rattrapée).
Corps adapté (client, joueur assis) : checks = {"fit_play"} — 0 point du corps hors habitacle, posé sur le siège, échelles lues. Refaire seats_play le véhicule posé sur une pente légère (plateforme inclinée
temporaire dans le dossier de travail).
Play, client (le runtime de rendu ne tourne que côté client ; loadstring y est interdit) :
start_stop_play(is_start = true).execute_luauClient :require(game.ReplicatedStorage.rbxts_include.F2R_Transfer.Verify)({ model = workspace.F2R_ImportTest:WaitForChild("<nom>"), checks = {"joints","lights","sounds","audio"} })→ rendu attaché tout seul parFiveMVehiclesClient(sinon : véhicule inerte en jeu), portes/capot/coffre à l'angle deHingesviaDoor_*, roues et volant via la télémétrie, étriers (hub_*) qui braquent sans tourner, bras de suspension (suspension_*) fixes en rotation, chaque LightRole en Neon (ou sa lumière allumée) pour Headlights 1/2, Brake, Reverse, Indicator Left/Right/Hazard, Engine, Siren ; sons chargés ; moteur audible.- Conduite :
execute_luauServer — asseoir le joueur (seat:Sit(humanoid)surSeats.DriverSeat) et écouter l'attributHorn;player_inputH puis E (maintenu 700 ms) ;execute_luauServer — attendreEngine = true,Headlights = 1,Hornvutruepuis revenufalse. - Moteur de conduite : joueur au volant (
VehicleAccess.Entercôté serveur), puisexecute_luauClientrequire(…Verify)({ model = …, checks = {"drive_play"} })(accélère, régime cohérent, s'arrête, reste droit). Mesures complètes (0-100, freinage, virages, bordure, côte, marche arrière) : pisteTestTrack.luacréée en Play loin de la carte, bancDriveBench.lua, voirreferences/roblox-integration.md(« Essais de conduite ») ; essais longs entask.spawn. start_stop_play(is_start = false). Toujours arrêter le Play avant de rendre la main ; un Play sans appel Studio pendant 3 min est arrêté par VibeStarter.
Lire les résultats de Verify, pas le nombre d'erreurs de console : une place qui contient d'autres
véhicules peut afficher des centaines de « Failed to load sound … » qui ne sont pas les nôtres.
Un statut warn « rendu figé » = Studio sans focus (RenderStepped arrêté) : relancer le groupe.
7. Corriger chaque échec
| Échec Verify | Cause habituelle | Correction |
|---|---|---|
| pièces manquantes / renommées | importeur différent | vérifier les noms (F2RMissingParts) ; réimporter le GLB (§ 4b) |
| empattement / avant vers −Z / roues côté | modèle en miroir, repères mal choisis | message « Recalage imprécis » du Setup ; corriger pick_ref_parts ou la conversion d'axes |
| porte à 0° (pas « rendu figé ») | Motor6D absent, pièce soudée à la racine | vérifier Joints/ et le propriétaire (Owner) des pièces de la porte |
| rôle de lumière resté éteint | pièce sans LightRole, ID de lumière inconnu | references/gta-format.md (IDs de lumières) ; corriger f2r/vehicle.py |
| SoundId vides / sons non chargés | upload non fait ou refusé | § 2 ; ApplySoundIds |
| Serialize / Apply | état non restauré | roblox/runtime/VehicleCustomization.lua |
hitbox : précision Default | ApplyMesh refusé (mesh non chargé, droits) | relancer le Setup ; vérifier que le GLB est un asset du propriétaire de la place |
| hitbox fidèle < 80 % | collision du mod grossière ou absente | lire « trop large / trop juste » ; mod sans Physics → boîte (signaler) |
| rendu non attaché par FiveMVehiclesClient | runtime d'avant le correctif du 2026-11-10 | réinstaller FiveMRuntime (nouvelle version) |
| étrier figé quand la roue braque | conversion d'avant le 2026-11-11 (os hub_* ignoré) | reconvertir et réinstaller (§ 4b à 6) |
| invites / sons « hors du véhicule » | Setup d'avant le 2026-11-11 (WorldPosition réglé avant le parent) | réinstaller le véhicule (§ 4b à 6) |
| descente : posé sur le toit | aucun espace libre des deux côtés (mur, autre véhicule) | normal ; sinon vérifier la pente et les pièces voisines |
| tête dans le toit (avertissement) | avatar trop grand pour l'échelle | reconvertir avec l'échelle conseillée par seats_play |
Après une correction du skill : reconvertir, réinstaller (supprimer le modèle, § 4b à 6) et revérifier.
Une modification du runtime se propage par FiveMRuntime (version = empreinte des sources).
8. Ranger et rendre compte
python scripts/studio_stage.py --clean --project <dossier>(retireF2R_Transfer).- Laisser le véhicule dans
Workspace.F2R_ImportTest, Play arrêté, caméra rendue. - Rapport final court, en français : ce qui marche (Verify : réussis / échecs / sans objet, test de
conduite), avertissements (textures ou sons non repris du jeu, rôles sans pièce, licence et marques),
ce qui reste à faire (calibrer la conduite sur des mesures FiveM, déplacer le modèle dans
ServerStorage, sons manquants…). Joindre une capture du véhicule.
Ce que l'utilisateur obtient dans Roblox
ModelavecPrimaryPart = VehicleRoot(ancré au départ),Hitbox,Visual/(MeshParts par propriétaire),Joints/(Motor6D : roues, portes, capot, coffre, volant, gyrophares),Seats/(DriverSeat…),Liveries/,Neons/, sons, lumières,VehicleConfig(ModuleScript), tagFiveMVehicle.ReplicatedStorage.FiveMVehicles:VehicleInput(contrôles),VehicleRuntime(rendu client),VehicleCustomization(API custom serveur), remotes ;FiveMVehiclesServer;FiveMVehiclesClient.- Contrôles par défaut (GTA-like) : F monter / descendre (invite par siège, côté portière ; manette Y ; bouton à l'écran sur mobile), W/S/A/D, Espace frein à main (un passager qui saute descend aussi), E klaxon, H phares (off/codes/pleins), ←/→ clignotants, ↓ warnings, Q sirène, R tonalité, Z moteur, G portes, X remettre sur ses roues (manette : croix bas), Maj / Ctrl rapports (manette : croix haut / X).
- Monter / descendre (
VehicleAccess, serveur) : distance, place libre et verrou (Locked) vérifiés, portière ouverte puis refermée, joueur assis sans marcher ; à la descente il est posé au sol à côté de sa portière, dans un espace libre (sinon l'autre côté, sinon le toit). Le client se pose lui-même : c'est lui qui possède la physique de son personnage. Les occupants ne heurtent pas la collision du véhicule (groupesFiveMVehicle/FiveMVehicleOccupant) jusqu'à la fermeture de la portière. - Stationnement : sans conducteur et à l'arrêt depuis
ParkAnchorDelays (5),VehicleRootest ancré (Parked) ; il est désancré et confié au conducteur quand celui-ci monte. Remise sur les roues : retourné ou sur le côté plus de 2 s et quasi immobile →Flipped, invite « Remettre sur ses roues » (F maintenu) ou X à bord ; délaiRecoverCooldown(5 s). Chute dans le vide : remis à sa dernière position sûre avant que Roblox ne détruise ses pièces (occupants compris,VoidRecoveries). - Conduite (
VehicleDynamics+VehicleDrive, client du conducteur) : le véhicule roule dès qu'un joueur prend le volant (boîte, freins, ABS, frein à main, suspension par rayons, son et compte-tours) ;UseBuiltinDrive = falsepour brancher un autre châssis. Mesures :docs/drive-tests.mddu dépôt. - Corps adapté à l'habitacle (
VehicleSeatFit, automatique) : le convertisseur voxelise l'habitacle (CONFIG.Cabin, 4 cm, ≈ 7 Ko) ; à l'assise, le client de l'occupant mesure son corps et cherche le réglage qui le garde entièrement dedans — corps le plus grand, puis le plus large, puis siège le moins reculé, puis le plus bas — et le serveur l'applique (R15 :BodyHeight/Width/Depth/HeadScale; R6 :ScaleTo), recale la soudure du siège (hauteur, recul) et masque les accessoires qui dépassent encore. Tout est rendu à la descente.SeatFit = falsesur le modèle pour désactiver ;--no-seat-fità la conversion. Habitacle ouvert (cabriolet) : pas d'adaptation. La posture assise reste celle de Roblox (tibias verticaux) : dans un coupé bas, le corps est fortement réduit (mesures :docs/seat-fit-tests.md). - Taille de l'avatar : à l'échelle réelle (3,571 studs/m) un avatar R6 assis touche souvent le pavillon.
Verify (
seats_play) mesure la garde au toit et donne l'échelle conseillée (--studs-per-meter).
Sans Studio connecté (livraison manuelle)
Livrer <sortie>/<nom>/ (sans _work/) zippé, avec :
- Importer 3D →
<nom>.glb; - sélectionner le modèle importé → Modèle › Exécuter un script →
<nom>_Setup.lua(version complète, runtime inclus) ; - le moteur de conduite est inclus ; pour un autre châssis :
references/roblox-integration.md(« Interface d'adaptation »). Résumer : pièces/triangles, mods, livrées, sons, avertissements.
Références (à lire au besoin)
references/gta-format.md— os, IDs de lumières, couches de peinture, shaders, metas, roues, mods, livrées.references/roblox-integration.md— hiérarchie produite, attributs, API Input/Runtime/Customization, branchement physique.references/troubleshooting.md— problèmes fréquents et corrections.references/game-assets.md— récupération des textures/sons du jeu de base.tools/gta-asset-server/README.md— serveur d'assets à lancer sur le PC.docs/transport-tests.md(canal VM → PC) etdocs/studio-import-tests.md(arrivée dans Studio, Verify).