10
10
11
11
class Flatout2 (ProtocolBase ):
12
12
"""
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
+
14
23
The protocol uses broadcast packets to discover and query servers.
15
24
"""
16
25
@@ -25,67 +34,54 @@ class Flatout2(ProtocolBase):
25
34
COMMAND_QUERY = b"\x18 \x0c "
26
35
PACKET_END = b"\x2e \x55 \x19 \xb4 \xe1 \x4f \x81 \x4a "
27
36
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
63
45
}
64
46
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
73
53
}
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
82
77
}
83
78
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)
89
85
}
90
86
91
87
@@ -276,16 +272,25 @@ def _read_utf16_string(self, br: BinaryReader) -> str:
276
272
277
273
def _extract_car_type (self , data : bytes ) -> str :
278
274
"""
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 .
281
277
282
278
: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
284
280
"""
285
281
try :
286
282
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)"
289
294
else :
290
295
return "Unknown"
291
296
except Exception as e :
@@ -294,15 +299,18 @@ def _extract_car_type(self, data: bytes) -> str:
294
299
295
300
def _extract_car_type_base (self , data : bytes ) -> str :
296
301
"""
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)
298
303
299
304
:param data: The complete response data
300
305
:return: The base car type name or "Unknown" if not found
301
306
"""
302
307
try :
303
308
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} )" )
306
314
else :
307
315
return "Unknown"
308
316
except Exception as e :
@@ -311,16 +319,18 @@ def _extract_car_type_base(self, data: bytes) -> str:
311
319
312
320
def _extract_upgrade_setting (self , data : bytes ) -> str :
313
321
"""
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)
316
323
317
324
:param data: The complete response data
318
325
:return: The upgrade setting or "Unknown" if not found
319
326
"""
320
327
try :
321
328
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} )" )
324
334
else :
325
335
return "Unknown"
326
336
except Exception as e :
@@ -329,22 +339,94 @@ def _extract_upgrade_setting(self, data: bytes) -> str:
329
339
330
340
def _extract_game_mode (self , data : bytes ) -> str :
331
341
"""
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 .
334
344
335
345
:param data: The complete response data
336
346
:return: The game mode name or "Unknown" if not found
337
347
"""
338
348
try :
339
349
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} )" )
342
355
else :
343
356
return "Unknown"
344
357
except Exception as e :
345
358
print (f"Error extracting game mode: { e } " )
346
359
return "Unknown"
347
360
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
+
348
430
def _extract_map_name (self , data : bytes , server_name : str ) -> str :
349
431
"""
350
432
Extracts the map name from the payload data.
@@ -468,6 +550,16 @@ def _parse_response(self, br: BinaryReader, original_data: bytes) -> Status:
468
550
game_mode = self ._extract_game_mode (original_data )
469
551
info ["game_mode" ] = game_mode
470
552
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
+
471
563
# Extract map information from the payload
472
564
# Map ID at offset 95, Track Type at offset 94
473
565
map_name = self ._extract_map_name (original_data , server_name )
@@ -539,6 +631,9 @@ def _parse_response(self, br: BinaryReader, original_data: bytes) -> Status:
539
631
info .setdefault ("car_type_base" , "Unknown" )
540
632
info .setdefault ("upgrade_setting" , "Unknown" )
541
633
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
542
637
info .setdefault ("map" , "Unknown Map" )
543
638
info .setdefault ("lap_count" , None )
544
639
info .setdefault ("time_limit" , None )
0 commit comments