1+ using System . Globalization ;
2+
13using CitizenFX . Base ;
24using CitizenFX . FiveM . Client ;
35using CitizenFX . FiveM . Client . Extensions ;
@@ -13,57 +15,231 @@ namespace vMenu.Enhanced.Menus;
1315public sealed class VehicleSpawnerMenu
1416{
1517 internal static NativeApi nativeApi = BaseEntrypoint . NativeApi ;
18+ private static readonly TextInfo _textInfo = new CultureInfo ( "en-US" , false ) . TextInfo ;
1619
17-
18- public async Task < Menu > GetMenu ( )
20+ public async Task Initialize ( )
1921 {
20-
2122 var menu = new Menu ( "Vehicle Spawner" , "Vehicle Spawner Menu" ) ;
2223 MenuController . AddSubmenu ( MenuController . MainMenu , menu ) ;
23- var linkBtn = new MenuItem ( "VehicleSpawner Menu" , "Temporary vehicle spawner menu." ) { Label = "»»» " } ;
24+ var linkBtn = new MenuItem ( "VehicleSpawner Menu" , "Temporary vehicle spawner menu." ) { Label = "→ " } ;
2425 MenuController . MainMenu . AddMenuItem ( linkBtn ) ;
2526 MenuController . BindMenuItem ( MenuController . MainMenu , menu , linkBtn ) ;
2627
27- var vehicles = BrokenNatives . NativeFixer . GetAllVehicleModels ( ) ;
28-
28+ MenuItem spawnByClassBtn = new MenuItem ( "Spawn Vehicle By Class" , "Spawn a vehicle from a list of vehicle classes." ) { Label = "→" } ;
29+ Menu spawnByClassMenu = new Menu ( "Vehicle Spawner" , "Spawn vehicles by class" ) ;
30+ menu . AddMenuItem ( spawnByClassBtn ) ;
31+ MenuController . AddSubmenu ( menu , spawnByClassMenu ) ;
32+ MenuController . BindMenuItem ( menu , spawnByClassMenu , spawnByClassBtn ) ;
2933
34+ var vehicles = BrokenNatives . NativeFixer . GetAllVehicleModels ( ) ;
3035
3136 if ( vehicles is null )
3237 {
33- return menu ;
38+ return ;
3439 }
3540
36- var vehicleSperCategory = vehicles
41+ var vehiclesPerCategory = vehicles
3742 . Where ( veh => ! string . IsNullOrWhiteSpace ( veh ) )
3843 . Select ( veh => veh . Trim ( ) )
3944 . OrderBy ( vehicle => vehicle )
40- . GroupBy ( vehicle => Native . GetVehicleClassFromName ( API . Hash ( vehicle ) ) ) ;
45+ . GroupBy ( vehicle => Native . GetVehicleClassFromName ( API . Hash ( vehicle ) ) )
46+ . OrderBy ( vehicle => Native . GetLabelText ( $ "VEH_CLASS_{ vehicle . Key } ") ) ;
4147
42- foreach ( var cat in vehicleSperCategory )
48+ // These are the max speed, acceleration, braking and traction values per vehicle class.
49+ var speedValues = new float [ 23 ]
50+ {
51+ 44.9374657f ,
52+ 50.0000038f ,
53+ 48.862133f ,
54+ 48.1321335f ,
55+ 50.7077942f ,
56+ 51.3333359f ,
57+ 52.3922348f ,
58+ 53.86687f ,
59+ 52.03867f ,
60+ 49.2241631f ,
61+ 39.6176529f ,
62+ 37.5559425f ,
63+ 42.72843f ,
64+ 21.0f ,
65+ 45.0f ,
66+ 65.1952744f ,
67+ 109.764259f ,
68+ 42.72843f ,
69+ 56.5962219f ,
70+ 57.5398865f ,
71+ 43.3140678f ,
72+ 26.66667f ,
73+ 53.0537224f
74+ } ;
75+ var accelerationValues = new float [ 23 ]
76+ {
77+ 0.34f ,
78+ 0.29f ,
79+ 0.335f ,
80+ 0.28f ,
81+ 0.395f ,
82+ 0.39f ,
83+ 0.66f ,
84+ 0.42f ,
85+ 0.425f ,
86+ 0.475f ,
87+ 0.21f ,
88+ 0.3f ,
89+ 0.32f ,
90+ 0.17f ,
91+ 18.0f ,
92+ 5.88f ,
93+ 21.0700016f ,
94+ 0.33f ,
95+ 14.0f ,
96+ 6.86f ,
97+ 0.32f ,
98+ 0.2f ,
99+ 0.76f
100+ } ;
101+ var brakingValues = new float [ 23 ]
102+ {
103+ 0.72f ,
104+ 0.95f ,
105+ 0.85f ,
106+ 0.9f ,
107+ 1.0f ,
108+ 1.0f ,
109+ 1.3f ,
110+ 1.25f ,
111+ 1.52f ,
112+ 1.1f ,
113+ 0.6f ,
114+ 0.7f ,
115+ 0.8f ,
116+ 3.0f ,
117+ 0.4f ,
118+ 3.5920403f ,
119+ 20.58f ,
120+ 0.9f ,
121+ 2.93960738f ,
122+ 3.9472363f ,
123+ 0.85f ,
124+ 5.0f ,
125+ 1.3f
126+ } ;
127+ var tractionValues = new float [ 23 ]
43128 {
44- var submenu = new Menu ( "Class: " + cat . Key . ToString ( ) , "Vehicle Spawner Menu" ) ;
129+ 2.3f ,
130+ 2.55f ,
131+ 2.3f ,
132+ 2.6f ,
133+ 2.625f ,
134+ 2.65f ,
135+ 2.8f ,
136+ 2.782f ,
137+ 2.9f ,
138+ 2.95f ,
139+ 2.0f ,
140+ 3.3f ,
141+ 2.175f ,
142+ 2.05f ,
143+ 0.0f ,
144+ 1.6f ,
145+ 2.15f ,
146+ 2.55f ,
147+ 2.57f ,
148+ 3.7f ,
149+ 2.05f ,
150+ 2.5f ,
151+ 3.2925f
152+ } ;
153+
154+ foreach ( var cat in vehiclesPerCategory )
155+ {
156+ var className = Native . GetLabelText ( $ "VEH_CLASS_{ cat . Key } ") ;
157+
158+ var vehicleClassSubMenu = new Menu ( className , "Vehicle Spawner Menu" ) ;
45159 foreach ( var vehicle in cat )
46160 {
47- submenu . AddMenuItem ( new MenuItem ( vehicle ) ) ;
161+ var hash = API . Hash ( vehicle ) ;
162+ var topSpeed = Map ( Native . GetVehicleModelEstimatedMaxSpeed ( hash ) , 0f , speedValues [ cat . Key ] , 0f , 1f ) ;
163+ var acceleration = Map ( Native . GetVehicleModelAcceleration ( hash ) , 0f , accelerationValues [ cat . Key ] , 0f , 1f ) ;
164+ var maxBraking = Map ( Native . GetVehicleModelMaxBraking ( hash ) , 0f , brakingValues [ cat . Key ] , 0f , 1f ) ;
165+ var maxTraction = Map ( Native . GetVehicleModelMaxTraction ( hash ) , 0f , tractionValues [ cat . Key ] , 0f , 1f ) ;
166+
167+ var vehicleDisplayName = GetVehicleDisplayName ( hash ) ;
168+ var vehicleSpawnButton = new MenuItem ( vehicleDisplayName )
169+ {
170+ Label = vehicle ,
171+ ItemData = new float [ 4 ] { topSpeed , acceleration , maxBraking , maxTraction }
172+ } ;
173+
174+ vehicleClassSubMenu . AddMenuItem ( vehicleSpawnButton ) ;
48175 }
49176
50- submenu . OnItemSelect += Submenu_OnItemSelect ;
51- MenuController . AddSubmenu ( menu , submenu ) ;
177+ static void HandleStatsPanel ( Menu openedMenu , MenuItem currentItem )
178+ {
179+ if ( currentItem != null )
180+ {
181+ if ( currentItem . ItemData is float [ ] data )
182+ {
183+ openedMenu . ShowVehicleStatsPanel = true ;
184+ openedMenu . SetVehicleStats ( data [ 0 ] , data [ 1 ] , data [ 2 ] , data [ 3 ] ) ;
185+ openedMenu . SetVehicleUpgradeStats ( 0f , 0f , 0f , 0f ) ;
186+ }
187+ else
188+ {
189+ openedMenu . ShowVehicleStatsPanel = false ;
190+ }
191+ }
192+ }
193+
194+ vehicleClassSubMenu . OnMenuOpen += ( m ) =>
195+ {
196+ HandleStatsPanel ( m , m . GetCurrentMenuItem ( ) ) ;
197+ } ;
198+
199+ vehicleClassSubMenu . OnIndexChange += ( m , oldItem , newItem , oldIndex , newIndex ) =>
200+ {
201+ HandleStatsPanel ( m , newItem ) ;
202+ } ;
203+
204+ vehicleClassSubMenu . OnItemSelect += Submenu_OnItemSelect ;
52205
53- var btn = new MenuItem ( "Veh Class: " + cat . Key . ToString ( ) ) ;
54- menu . AddMenuItem ( btn ) ;
206+ var btn = new MenuItem ( className , $ "Spawn a vehicle from the ~y~{ className } ~s~ class.") { Label = $ "({ cat . Count ( ) } ) →" } ;
207+ spawnByClassMenu . AddMenuItem ( btn ) ;
208+ MenuController . AddSubmenu ( spawnByClassMenu , vehicleClassSubMenu ) ;
209+ MenuController . BindMenuItem ( spawnByClassMenu , vehicleClassSubMenu , btn ) ;
210+ }
211+ }
55212
56- MenuController . BindMenuItem ( menu , submenu , btn ) ;
57- API . Log . Info ( "Added submenu for vehicle class: {0}" , cat . Key . ToString ( ) ) ;
213+ private static string GetVehicleDisplayName ( uint hash )
214+ {
215+ var displayName = Native . GetDisplayNameFromVehicleModel ( hash ) ;
216+ var labelText = Native . GetLabelText ( displayName ) ;
217+ if ( labelText == "NULL" )
218+ {
219+ return _textInfo . ToTitleCase ( displayName ) ;
58220 }
59221
60- return menu ;
222+ return _textInfo . ToTitleCase ( labelText ) ;
223+ }
224+
225+ /// <summary>
226+ /// Maps the <paramref name="value"/> (which is a value between <paramref name="min_in"/> and <paramref name="max_in"/>) to a new value in the range of <paramref name="min_out"/> and <paramref name="max_out"/>.
227+ /// </summary>
228+ /// <param name="value">The value to map.</param>
229+ /// <param name="min_in">The minimum range value of the value.</param>
230+ /// <param name="max_in">The max range value of the value.</param>
231+ /// <param name="min_out">The min output range value.</param>
232+ /// <param name="max_out">The max output range value.</param>
233+ /// <returns></returns>
234+ public static float Map ( float value , float min_in , float max_in , float min_out , float max_out )
235+ {
236+ return ( ( value - min_in ) * ( max_out - min_out ) / ( max_in - min_in ) ) + min_out ;
61237 }
62238
63239
64240 private async void Submenu_OnItemSelect ( Menu menu , MenuItem menuItem , int itemIndex )
65241 {
66- var hash = API . Hash ( menuItem . Text ) ;
242+ var hash = API . Hash ( menuItem . Label ) ;
67243
68244 // Manually checking and requesting model because API.Vehicles.RequestAndCreate uses datetime which is currently broken and crashes the game.
69245 // https://github.com/citizenfx/rfc/discussions/328
@@ -79,10 +255,28 @@ private async void Submenu_OnItemSelect(Menu menu, MenuItem menuItem, int itemIn
79255 await API . Delay ( 0 ) ;
80256 }
81257
82- var veh = await API . Vehicles . RequestAndCreate ( hash , API . Players . Local . Ped ! . Position , ( int ) API . Players . Local . Ped . Heading , true , true , true ) ;
83- if ( veh is not null )
258+ var ped = API . Players . Local . Ped ! ;
259+
260+ var position = ped . Position ;
261+ if ( ped . IsPedInAnyVehicle ( ) )
84262 {
85- API . Players . Local . Ped . SetPedIntoVehicle ( veh ! . Handle , - 1 ) ;
263+ var currentVehicle = ped . Vehicle ! ;
264+
265+ BrokenNatives . NativeFixer . GetModelDimensions ( currentVehicle . Model , out var p1 , out var p2 ) ;
266+ BrokenNatives . NativeFixer . GetModelDimensions ( hash , out var p3 , out var p4 ) ;
267+
268+ var yOffset = ( Math . Abs ( ( p1 - p2 ) . Y ) / 2 ) + ( Math . Abs ( ( p3 - p4 ) . Y ) / 2 ) + 1f ;
269+ position = Native . GetOffsetFromEntityInWorldCoords ( currentVehicle . Handle , 0f , yOffset , 0f ) ;
86270 }
271+ var newVehicle = await API . Vehicles . RequestAndCreate ( hash , position , ( int ) ped . Heading , true , true , true ) ;
272+
273+ Native . SetModelAsNoLongerNeeded ( hash ) ;
274+
275+ if ( newVehicle is null )
276+ {
277+ return ;
278+ }
279+
280+ ped . SetPedIntoVehicle ( newVehicle ! . Handle , - 1 ) ;
87281 }
88282}
0 commit comments