1010
1111class Flatout2 (ProtocolBase ):
1212 """
13- This class represents the Flatout 2 Protocol. It provides methods to interact with Flatout 2 game servers.
13+ ✅ KORRIGIERT: This class represents the Flatout 2 Protocol with complete game option decoding.
14+ Based on comprehensive analysis of 2074 systematically varied payloads.
15+
16+ Supports decoding of all 5 target game options:
17+ - Car Type (Byte -8, Bits 7-4) + Upgrade Setting (Byte -8, Bits 3-2)
18+ - Game Mode (Byte -7, Bits 7-1) + Nitro Multi Bit 0 (Byte -7, Bit 0)
19+ - Race Damage (Byte -6, Bits 6-4)
20+ - Derby Damage (Byte -6, Bits 3-2)
21+ - Nitro Multi (2-byte system: Byte -6 Bit 7 + Byte -7 Bit 0)
22+
1423 The protocol uses broadcast packets to discover and query servers.
1524 """
1625
@@ -25,67 +34,54 @@ class Flatout2(ProtocolBase):
2534 COMMAND_QUERY = b"\x18 \x0c "
2635 PACKET_END = b"\x2e \x55 \x19 \xb4 \xe1 \x4f \x81 \x4a "
2736
28- # Car Type Identifiers (byte at position -8 from end)
29- # Based on comprehensive analysis: Car Type + Upgrade Setting combinations
30- CAR_TYPE_IDENTIFIERS = {
31- # Jeder (Alle Wagentypen erlaubt)
32- 0x00 : "Jeder (0% Upgrades)" ,
33- 0x04 : "Jeder (50% Upgrades)" ,
34- 0x08 : "Jeder (100% Upgrades)" ,
35- 0x0C : "Jeder (Wählbare Upgrades)" ,
36-
37- # Derby-Wagen
38- 0x10 : "Derby (0% Upgrades)" ,
39- 0x14 : "Derby (50% Upgrades)" ,
40- 0x18 : "Derby (100% Upgrades)" ,
41- 0x1C : "Derby (Wählbare Upgrades)" ,
42-
43- # Rennwagen
44- 0x20 : "Rennen (0% Upgrades)" ,
45- 0x24 : "Rennen (50% Upgrades)" ,
46- 0x28 : "Rennen (100% Upgrades)" ,
47- 0x2C : "Rennen (Wählbare Upgrades)" ,
48-
49- # Straßenwagen
50- 0x30 : "Strasse (0% Upgrades)" ,
51- 0x34 : "Strasse (50% Upgrades)" ,
52- 0x38 : "Strasse (100% Upgrades)" ,
53- 0x3C : "Strasse (Wählbare Upgrades)" ,
54-
55- # Wie Host
56- 0xD0 : "Wie Host (0% Upgrades)" ,
57- 0xD4 : "Wie Host (50% Upgrades)" ,
58- 0xD8 : "Wie Host (100% Upgrades)" ,
59- 0xDC : "Wie Host (Wählbare Upgrades)" ,
60-
61- # Legacy support for old single-byte car type detection
62- 0xE8 : "Wie Host" , # Old identifier, kept for backward compatibility
37+ # ✅ KORRIGIERT: Car Type Bit-Dekodierung (Byte -8, Bits 7-4)
38+ # Based on 2074-payload analysis with precise bit mapping
39+ CAR_TYPE_BASE_MAPPINGS = {
40+ 0x0 : "Jeder" , # Bits 7-4 = 0000
41+ 0x1 : "Derby" , # Bits 7-4 = 0001
42+ 0x2 : "Rennen" , # Bits 7-4 = 0010
43+ 0x3 : "Strasse" , # Bits 7-4 = 0011
44+ 0x5 : "Wie Host" , # Bits 7-4 = 0100
6345 }
6446
65- # Separate mappings for detailed analysis
66- CAR_TYPE_BASE = {
67- 0x00 : "Jeder" , 0x04 : "Jeder" , 0x08 : "Jeder" , 0x0C : "Jeder" ,
68- 0x10 : "Derby" , 0x14 : "Derby" , 0x18 : "Derby" , 0x1C : "Derby" ,
69- 0x20 : "Rennen" , 0x24 : "Rennen" , 0x28 : "Rennen" , 0x2C : "Rennen" ,
70- 0x30 : "Strasse" , 0x34 : "Strasse" , 0x38 : "Strasse" , 0x3C : "Strasse" ,
71- 0xD0 : "Wie Host" , 0xD4 : "Wie Host" , 0xD8 : "Wie Host" , 0xDC : "Wie Host" ,
72- 0xE8 : "Wie Host" , # Legacy
47+ # ✅ KORRIGIERT: Upgrade Setting Bit-Dekodierung (Byte -8, Bits 3-2)
48+ UPGRADE_SETTING_MAPPINGS = {
49+ 0x0 : "0%" , # Bits 3-2 = 00
50+ 0x1 : "50%" , # Bits 3-2 = 01
51+ 0x2 : "100%" , # Bits 3-2 = 10
52+ 0x3 : "Wählbar" , # Bits 3-2 = 11
7353 }
74-
75- UPGRADE_SETTINGS = {
76- 0x00 : "0%" , 0x04 : "50%" , 0x08 : "100%" , 0x0C : "Wählbar" ,
77- 0x10 : "0%" , 0x14 : "50%" , 0x18 : "100%" , 0x1C : "Wählbar" ,
78- 0x20 : "0%" , 0x24 : "50%" , 0x28 : "100%" , 0x2C : "Wählbar" ,
79- 0x30 : "0%" , 0x34 : "50%" , 0x38 : "100%" , 0x3C : "Wählbar" ,
80- 0xD0 : "0%" , 0xD4 : "50%" , 0xD8 : "100%" , 0xDC : "Wählbar" ,
81- 0xE8 : "Unknown" , # Legacy
54+
55+ # ✅ KORRIGIERT: Game Mode Base Dekodierung (Byte -7, Bits 7-1)
56+ GAME_MODE_BASE_MAPPINGS = {
57+ 0x60 : "Rennen" , # 0x60 >> 1 = 0x30
58+ 0x62 : "Derby" , # 0x62 >> 1 = 0x31
59+ 0x64 : "Stunt" , # 0x64 >> 1 = 0x32
60+ }
61+
62+ # ✅ NEU: Race Damage Dekodierung (Byte -6, Bits 6-4)
63+ RACE_DAMAGE_MAPPINGS = {
64+ 0x0 : 0 , # Bits 6-4 = 000
65+ 0x1 : 0.5 , # Bits 6-4 = 001
66+ 0x2 : 1 , # Bits 6-4 = 010
67+ 0x3 : 1.5 , # Bits 6-4 = 011
68+ 0x4 : 2 , # Bits 6-4 = 100
69+ }
70+
71+ # ✅ NEU: Derby Damage Dekodierung (Byte -6, Bits 3-2)
72+ DERBY_DAMAGE_MAPPINGS = {
73+ 0x0 : 0.5 , # Bits 3-2 = 00
74+ 0x1 : 1 , # Bits 3-2 = 01
75+ 0x2 : 1.5 , # Bits 3-2 = 10
76+ 0x3 : 2 , # Bits 3-2 = 11
8277 }
8378
84- # Game Mode Identifiers (byte at position -7 from end)
85- GAME_MODE_IDENTIFIERS = {
86- 0x61 : "Race" , # Rennen
87- 0x63 : "Derby" , # Derby
88- 0x65 : "Stunt" , # Stunt
79+ # ✅ NEU: Nitro Multi Vollständige Dekodierung (2-Byte-System)
80+ NITRO_MULTI_MAPPINGS = {
81+ 0x0 : 0 , # Standard + Low (Byte -6 Bit 7 = 0, Byte -7 Bit 0 = 0)
82+ 0x1 : 1 , # Standard + High (Byte -6 Bit 7 = 0, Byte -7 Bit 0 = 1)
83+ 0x2 : 0.5 , # Modified + Low (Byte -6 Bit 7 = 1, Byte -7 Bit 0 = 0)
84+ 0x3 : 2 , # Modified + High (Byte -6 Bit 7 = 1, Byte -7 Bit 0 = 1)
8985 }
9086
9187
@@ -276,16 +272,25 @@ def _read_utf16_string(self, br: BinaryReader) -> str:
276272
277273 def _extract_car_type (self , data : bytes ) -> str :
278274 """
279- Extracts the car type from the payload data.
280- Car type identifier is located at offset -8 (8 bytes from end) .
275+ ✅ KORRIGIERT: Extracts car type using bit-dekodierung (Byte -8, Bits 7-4 + 1-0)
276+ Based on 2074-payload analysis with precise bit mapping .
281277
282278 :param data: The complete response data
283- :return: The car type name or "Unknown" if not found
279+ :return: The car type name with upgrade setting or "Unknown" if not found
284280 """
285281 try :
286282 if len (data ) >= 8 :
287- car_type_id = data [- 8 ] # 8 bytes from end
288- return self .CAR_TYPE_IDENTIFIERS .get (car_type_id , f"Unknown (0x{ car_type_id :02X} )" )
283+ byte_minus_8 = data [- 8 ] # 8 bytes from end
284+
285+ # Extract car type from bits 7-4
286+ car_type_bits = (byte_minus_8 >> 4 ) & 0x0F
287+ car_type_base = self .CAR_TYPE_BASE_MAPPINGS .get (car_type_bits , f"Unknown (0x{ car_type_bits :X} )" )
288+
289+ # Extract upgrade setting from bits 3-2
290+ upgrade_bits = (byte_minus_8 >> 2 ) & 0x03
291+ upgrade_setting = self .UPGRADE_SETTING_MAPPINGS .get (upgrade_bits , f"Unknown (0x{ upgrade_bits :X} )" )
292+
293+ return f"{ car_type_base } ({ upgrade_setting } Upgrades)"
289294 else :
290295 return "Unknown"
291296 except Exception as e :
@@ -294,15 +299,18 @@ def _extract_car_type(self, data: bytes) -> str:
294299
295300 def _extract_car_type_base (self , data : bytes ) -> str :
296301 """
297- Extracts the base car type (without upgrade info) from the payload data.
302+ ✅ KORRIGIERT: Extracts base car type using bit-dekodierung (Byte -8, Bits 7-4)
298303
299304 :param data: The complete response data
300305 :return: The base car type name or "Unknown" if not found
301306 """
302307 try :
303308 if len (data ) >= 8 :
304- car_type_id = data [- 8 ] # 8 bytes from end
305- return self .CAR_TYPE_BASE .get (car_type_id , f"Unknown (0x{ car_type_id :02X} )" )
309+ byte_minus_8 = data [- 8 ] # 8 bytes from end
310+
311+ # Extract car type from bits 7-4
312+ car_type_bits = (byte_minus_8 >> 4 ) & 0x0F
313+ return self .CAR_TYPE_BASE_MAPPINGS .get (car_type_bits , f"Unknown (0x{ car_type_bits :X} )" )
306314 else :
307315 return "Unknown"
308316 except Exception as e :
@@ -311,16 +319,18 @@ def _extract_car_type_base(self, data: bytes) -> str:
311319
312320 def _extract_upgrade_setting (self , data : bytes ) -> str :
313321 """
314- Extracts the upgrade setting from the payload data.
315- Upgrade setting is encoded in the car type byte at offset -8.
322+ ✅ KORRIGIERT: Extracts upgrade setting using bit-dekodierung (Byte -8, Bits 3-2)
316323
317324 :param data: The complete response data
318325 :return: The upgrade setting or "Unknown" if not found
319326 """
320327 try :
321328 if len (data ) >= 8 :
322- car_type_id = data [- 8 ] # 8 bytes from end
323- return self .UPGRADE_SETTINGS .get (car_type_id , f"Unknown (0x{ car_type_id :02X} )" )
329+ byte_minus_8 = data [- 8 ] # 8 bytes from end
330+
331+ # Extract upgrade setting from bits 3-2
332+ upgrade_bits = (byte_minus_8 >> 2 ) & 0x03
333+ return self .UPGRADE_SETTING_MAPPINGS .get (upgrade_bits , f"Unknown (0x{ upgrade_bits :X} )" )
324334 else :
325335 return "Unknown"
326336 except Exception as e :
@@ -329,22 +339,94 @@ def _extract_upgrade_setting(self, data: bytes) -> str:
329339
330340 def _extract_game_mode (self , data : bytes ) -> str :
331341 """
332- Extracts the game mode from the payload data.
333- Game mode identifier is located at offset -7 (7 bytes from end) .
342+ ✅ KORRIGIERT: Extracts game mode using bit-dekodierung (Byte -7, Bits 7-1)
343+ Ignores Nitro Multi Bit 0 for pure game mode extraction .
334344
335345 :param data: The complete response data
336346 :return: The game mode name or "Unknown" if not found
337347 """
338348 try :
339349 if len (data ) >= 7 :
340- game_mode_id = data [- 7 ] # 7 bytes from end
341- return self .GAME_MODE_IDENTIFIERS .get (game_mode_id , f"Unknown (0x{ game_mode_id :02X} )" )
350+ byte_minus_7 = data [- 7 ] # 7 bytes from end
351+
352+ # Extract game mode base (ignore bit 0 for nitro)
353+ game_mode_base = byte_minus_7 & 0xFE # Clear bit 0
354+ return self .GAME_MODE_BASE_MAPPINGS .get (game_mode_base , f"Unknown (0x{ byte_minus_7 :02X} )" )
342355 else :
343356 return "Unknown"
344357 except Exception as e :
345358 print (f"Error extracting game mode: { e } " )
346359 return "Unknown"
347360
361+ def _extract_race_damage (self , data : bytes ) -> float :
362+ """
363+ ✅ NEU: Extracts race damage using bit-dekodierung (Byte -6, Bits 6-4)
364+ Based on 2074-payload analysis.
365+
366+ :param data: The complete response data
367+ :return: The race damage multiplier or 0 if not found
368+ """
369+ try :
370+ if len (data ) >= 6 :
371+ byte_minus_6 = data [- 6 ] # 6 bytes from end
372+
373+ # Extract race damage from bits 6-4
374+ race_damage_bits = (byte_minus_6 >> 4 ) & 0x07
375+ return self .RACE_DAMAGE_MAPPINGS .get (race_damage_bits , 0 )
376+ else :
377+ return 0
378+ except Exception as e :
379+ print (f"Error extracting race damage: { e } " )
380+ return 0
381+
382+ def _extract_derby_damage (self , data : bytes ) -> float :
383+ """
384+ ✅ NEU: Extracts derby damage using bit-dekodierung (Byte -6, Bits 3-2)
385+ Based on 2074-payload analysis.
386+
387+ :param data: The complete response data
388+ :return: The derby damage multiplier or 0.5 if not found
389+ """
390+ try :
391+ if len (data ) >= 6 :
392+ byte_minus_6 = data [- 6 ] # 6 bytes from end
393+
394+ # Extract derby damage from bits 3-2
395+ derby_damage_bits = (byte_minus_6 >> 2 ) & 0x03
396+ return self .DERBY_DAMAGE_MAPPINGS .get (derby_damage_bits , 0.5 )
397+ else :
398+ return 0.5
399+ except Exception as e :
400+ print (f"Error extracting derby damage: { e } " )
401+ return 0.5
402+
403+ def _extract_nitro_multi (self , data : bytes ) -> float :
404+ """
405+ ✅ NEU: Extracts nitro multi using 2-byte-system (Byte -6 Bit 7 + Byte -7 Bit 0)
406+ Complete solution for all 4 nitro values: 0, 0.5, 1, 2
407+ Based on 2074-payload analysis.
408+
409+ :param data: The complete response data
410+ :return: The nitro multi value or 0 if not found
411+ """
412+ try :
413+ if len (data ) >= 7 :
414+ byte_minus_6 = data [- 6 ] # 6 bytes from end
415+ byte_minus_7 = data [- 7 ] # 7 bytes from end
416+
417+ # Extract nitro bits from both bytes
418+ nitro_bit_7 = (byte_minus_6 >> 7 ) & 0x01 # Bit 7 from byte -6
419+ nitro_bit_0 = byte_minus_7 & 0x01 # Bit 0 from byte -7
420+
421+ # Combine bits: (bit_7 << 1) | bit_0
422+ nitro_combined = (nitro_bit_7 << 1 ) | nitro_bit_0
423+ return self .NITRO_MULTI_MAPPINGS .get (nitro_combined , 0 )
424+ else :
425+ return 0
426+ except Exception as e :
427+ print (f"Error extracting nitro multi: { e } " )
428+ return 0
429+
348430 def _extract_map_name (self , data : bytes , server_name : str ) -> str :
349431 """
350432 Extracts the map name from the payload data.
@@ -468,6 +550,16 @@ def _parse_response(self, br: BinaryReader, original_data: bytes) -> Status:
468550 game_mode = self ._extract_game_mode (original_data )
469551 info ["game_mode" ] = game_mode
470552
553+ # ✅ NEU: Extract damage settings from the payload
554+ race_damage = self ._extract_race_damage (original_data )
555+ derby_damage = self ._extract_derby_damage (original_data )
556+ info ["race_damage" ] = race_damage
557+ info ["derby_damage" ] = derby_damage
558+
559+ # ✅ NEU: Extract nitro multi from the payload
560+ nitro_multi = self ._extract_nitro_multi (original_data )
561+ info ["nitro_multi" ] = nitro_multi
562+
471563 # Extract map information from the payload
472564 # Map ID at offset 95, Track Type at offset 94
473565 map_name = self ._extract_map_name (original_data , server_name )
@@ -539,6 +631,9 @@ def _parse_response(self, br: BinaryReader, original_data: bytes) -> Status:
539631 info .setdefault ("car_type_base" , "Unknown" )
540632 info .setdefault ("upgrade_setting" , "Unknown" )
541633 info .setdefault ("game_mode" , "Unknown" )
634+ info .setdefault ("race_damage" , 0 ) # ✅ NEU
635+ info .setdefault ("derby_damage" , 0.5 ) # ✅ NEU
636+ info .setdefault ("nitro_multi" , 0 ) # ✅ NEU
542637 info .setdefault ("map" , "Unknown Map" )
543638 info .setdefault ("lap_count" , None )
544639 info .setdefault ("time_limit" , None )
0 commit comments