How I Reversed World of Tanks Blitz's Custom DVPL Format

# 2025-06-06 @reverse-engineering @file-formats

The Discovery

I was trying to learn SQL for a Database course exam for Uni and I decided to do that by making a tank database, since I play a lot of World of Tanks Blitz as a way to chill out.

I spent a few hours MANUALLY adding data, and I quickly ran out of it, since the Wiki only has partial data. I looked on other sites, reversed protobuf files, looked on the Main Wiki… Nothing helpful. I decided to check the game files, where I stumbled across .dvpl files. These weren’t standard formats I recognized, so naturally, I had to figure out what they were.

My first instinct was to dump strings from the files to see if there were any readable clues:

strings list.xml.dvpl | head -20
<root>
 <Ch01_Type59>
  <id>0</id
userString>#china_vehicles:8
'</(
description8
(<//
price>7500<gold/
QsellP
9375$
notInShop>true</
enrichmentPermanentCost
tags>mediumTank enhancedTorsions3t_
pCaliber*
bRammer
wetCombatPack_class1
aimingStabilizer_Mk
collectible</
level>8</
1Rol

Most of it was gibberish, but I could make out fragments of XML data (it’s in the file name, duh) mixed with compressed binary data. The presence of readable strings suggested the files contained processed data.

Binary Analysis: Finding the Magic

When dealing with unknown binary formats, I always start with a hex dump to look for patterns:

hexdump -C list.xml.dvpl | tail -10

This revealed something interesting at the end of the file:

00000ea0  11 36 af 0e 07 a8 7b 1f  35 f5 2f 14 06 3a 00 0f  |.6....{.5./..:..|
00000eb0  07 08 21 05 3d 00 0f 03  08 28 06 44 00 0f 5f 13  |..!.=....(.D.._.|
00000ec0  fe 0f 70 05 9c 09 86 02  00 96 02 00 80 05 35 54  |..p...........5T|
00000ed0  5f 37 51 70 0f 79 56 15  05 39 00 0f 94 02 21 04  |_7Qp.yV..9....!.|
00000ee0  3c 00 0f 93 02 28 05 43  00 0f 91 0a bc 10 20 90  |<....(.C...... .|
00000ef0  52 08 28 7c 02 77 2d 9f  20 6e 6f 52 61 74 69 6e  |R.(|.w-. noRatin|
00000f00  67 b1 0a db 06 9e 02 80  3c 2f 72 6f 6f 74 3e 0a  |g.......</root>.|
00000f10  84 86 00 00 10 0f 00 00  ac 01 2a 9f 02 00 00 00  |..........*.....|
00000f20  44 56 50 4c                                       |DVPL|
00000f24

There it was - the ASCII string “DVPL” at the very end of the file! This looked like a magic number, which often indicates a footer or header structure in custom file formats.

Analyzing the Footer Structure

With the magic number found, I needed to understand what came before it.

# Get the last 32 bytes to see the footer structure
tail -c 32 list.xml.dvpl | hexdump -C
00000000  06 9e 02 80 3c 2f 72 6f  6f 74 3e 0a 84 86 00 00  |....</root>.....|
00000010  10 0f 00 00 ac 01 2a 9f  02 00 00 00 44 56 50 4c  |......*.....DVPL|
00000020

The DVPL magic is exactly 4 bytes, and there are 16 bytes before it. This suggests a 20-byte footer structure. Looking at the pattern, it appears to be several 4-byte (32-bit) integers followed by the magic number.

Reverse Engineering the Structure

To understand what these integers represent, I compared several DVPL files to look for patterns. This is where things got interesting:

tail -c 32 customization.xml.dvpl | hexdump -C
00000000  0e 15 00 80 3c 2f 72 6f  6f 74 3e 0a 23 16 00 00  |....</root>.#...|
00000010  54 03 00 00 6e 18 a0 f9  02 00 00 00 44 56 50 4c  |T...n.......DVPL|
00000020
tail -c 32 Ch01_Type59.xml.dvpl | hexdump -C
00000000  03 8d 00 80 3c 2f 72 6f  6f 74 3e 0a 2f 2a 00 00  |....</root>./*..|
00000010  0f 0f 00 00 e8 c7 c4 31  02 00 00 00 44 56 50 4c  |.......1....DVPL|
00000020
tail -c 32 guns.xml.dvpl | hexdump -C
00000000  72 b0 00 80 3c 2f 72 6f  6f 74 3e 0a ae e4 01 00  |r...</root>.....|
00000010  90 27 00 00 a2 3d d5 7b  02 00 00 00 44 56 50 4c  |.'...=.{....DVPL|
00000020

Looking at these hex dumps, I noticed a consistent pattern:

  • 16 bytes of what looks like structured data
  • 4 bytes that spell “DVPL”
  • The 4 bytes immediately before “DVPL” are always 02 00 00 00 across all files

This suggested the footer contains four 32-bit little-endian integers followed by the magic number. The consistent 02 00 00 00 (which is 2 in little-endian) looked like it could be a compression type field.

Decoding the Structure

Based on this pattern, I hypothesized the footer structure as four integers. Now I needed to figure out what each one represented:

import struct

footer_bytes = bytes.fromhex("84860000100f0000ac012a9f02000000")
val1, val2, val3, val4 = struct.unpack('<IIII', footer_bytes)

print(f"Value 1: {val1}")
print(f"Value 2: {val2}")
print(f"Value 3: {val3}")
print(f"Value 4: {val4}")
Value 1: 34436
Value 2: 3856
Value 3: 2670330284
Value 4: 2

The fourth value being consistently 2 across all files strongly suggested a compression type. For the other values, I made guesses based on common file format patterns:

Typical compressed file formats often store:

  • Original (uncompressed) size
  • Compressed size
  • Checksum for integrity
  • Compression method/flags

Let me test this hypothesis by checking if the file sizes make sense:

ls -l list.xml.dvpl
-rwxr-xr-x 1 user user 3876 Jun  4 17:25 list.xml.dvpl*

The second value (3856) exactly matches the payload size (minus the 20-byte footer), confirming it’s the compressed size.

To verify the first value is the original size, I needed to decompress the data. The compression type of 2 suggested it might be LZ4 (a common game compression format):

import lz4.block

with open('list.xml.dvpl', 'rb') as f:
    payload = f.read(3856)  # Read just the compressed data

try:
    decompressed = lz4.block.decompress(payload, uncompressed_size=34436)
    print(f"Decompressed size: {len(decompressed)}")
    print(decompressed[:100].decode('utf-8'))
except Exception as e:
    print(f"LZ4 failed: {e}")

This worked perfectly! The decompressed data was exactly 34436 bytes and started with proper XML:

Decompressed size: 34436
<root>
 <Ch01_Type59>
  <id>0</id>
  <userString>#china_vehicles:Ch01_Type59</userString>
  <descrip

For the third value (2670330284), I suspected it was a CRC32 checksum of the compressed data:

import zlib

calculated_crc = zlib.crc32(payload) & 0xffffffff  # Ensure unsigned 32-bit
print(f"Calculated CRC32: {calculated_crc}")
print(f"Footer CRC32: {val3}")
print(f"Match: {calculated_crc == val3}")
Calculated CRC32: 2670330284
Footer CRC32: 2670330284
Match: True

Perfect match! So the complete structure is:

# DVPL footer format: <IIII4s
original_size, compressed_size, crc32_checksum, compression_type, magic = struct.unpack('<IIII4s', footer_data)

Or in simpler terms:

  • The <IIII4s format string tells Python how to interpret the binary data:
    • < = little-endian byte order (least significant byte first)
    • I = unsigned 32-bit integer (4 bytes each)
    • IIII = four consecutive 32-bit integers
    • 4s = exactly 4 bytes as a string (our “DVPL” magic)
  • So <IIII4s reads: “four little-endian integers followed by a 4-byte string”

The Complete DVPL Format

After this systematic analysis, I determined the DVPL format structure:

[Compressed Payload Data]
[Footer - 20 bytes:]
  - Original Size    (4 bytes, little-endian uint32)
  - Compressed Size  (4 bytes, little-endian uint32)
  - CRC32 Checksum   (4 bytes, little-endian uint32)
  - Compression Type (4 bytes, little-endian uint32)
    - 0: No compression
    - 1: LZ4 compression
    - 2: LZ4 High Compression
  - Magic Number (4 bytes, ASCII "DVPL")

The Code

You can find the complete DVPL converter implementation here (when I make it public).

# built with sveltekit + mdsvex | catppuccin mocha