User Profile

Kaylin

(no biography information)

Id 4997
Member for8 years, 4 months, 2 days
Comments made9
Documents authored0
News posted0
Packets authored0
Servers owned0

Comments

Kaylin
🔎

The 0x800 value sent for the dword unknown seems to be the same hardcore flag used in https://bnetdocs.org/packet/162/mcp-gameinfo. Softcore characters seem to request with 0, hardcore with 0x800 (checked SCNL, SCL, HCL all LoD). Further testing should probably be done to see if the server cares at all, if other flags work, or if it's possible to request game listings from modes you're not playing.

Kaylin
🔎

Compressed Diablo II game server headers are actually a variable length header.

This means the message is the following if the first byte is strictly less than 0xF0.

[Byte] Size of message including this header
(void) Compressed payload

and if the first byte is >= 0xF0 the format is:

[Byte] H1
[Byte] H2
(void) Compressed payload.

In this case compute the message length including the header by
computing (pseudo code)

length = ((H1 << 8) + H2) & 0xFFF

Decompressed Diablo II game server protocol looks like:

[Byte] Message ID
(void) Decompressed payload

Each compressed message can contain multiple messages in the payload once decompressed and a compressed message may be delivered in multiple reads. The compressed data is padded with zeroes to the next byte boundary. Message lengths are in most cases fixed and can be computed from the contents when they are variable length.

NOTE: Only D2GS S>C messages are compressed except message id 0xAF. C>S are not. See https://bnetdocs.org/packet/245/d2gs-startlogon for more information.

Kaylin
🔎

While this comment applies to every message that contains a hash it is especially pertinent to the authentication check message used by WC3/D2/SC because the distinction is more obvious. Hashes are not sequences of dwords. A hash is a bit-string. This means (Dword) Hashed Key Data[5] is in fact (Byte) Hashed Key Data[20]. I make the distinction here because both bsha and sha1 are used with this message. Specifically the 26-character product codes use sha1 and the 13/16 character product codes uses bsha. Implementations of bsha and sha1 are available across the internet so I won't explain them in detail here. It is important to note that while sha1 is represented in big endian, bsha is little endian. If you presume the output of these functions are dwords not bit strings you may not send the correct data since the BNCS protocol is itself little-endian. VERY IMPORTANT: When I say Dword Input for some variable passed to these functions you're going to have to put the least significant byte of that dword into the least significant byte of the internal state for bsha, and into the most significant byte for sha1 and so on in order to get the correct output.

In addition the hashed key data is not the same for the different key sizes. Note: I refer to dwords here because while the output is a bit string the internal state is indeed a set of dwords. The hashed data for bsha is as above, which I shall repeat here so the distinction between the 26-char keys is clear. Remember bsha is little endian so the least significant byte of the first resulting dword of the final internal state is the first byte of output.

Input for 16/13 character product key hashes:

(Dword) Client Nonce (Client Token)
(Dword) Server Nonce (Server Token)
(Dword) Product
(Dword) Public
(Dword) Zero
(Dword) Private

Now, the 26-character keys use sha1 NOT bsha. Note, sha1 is big-endian internally. The most significant byte of the first resulting dword of the final internal state should be sent across the wire first etc. (because hash outputs aren't dwords).

(Dword) Client Nonce
(Dword) Server Nonce
(Dword) Product
(Dword) Public
(Byte) Private[10]

Note the difference: there is no zero dword in the input to the hash function.

Kaylin
🔎

(Dword) status is one of a few special values indicating various failure replies or a set of flags indicating the game type and difficulty. Documentation on the reply codes can be found here since MCP_GAMEINFO uses the same codes: < https://bnetdocs.org/packet/162/mcp-gameinfo >

Kaylin
🔎

The formatting of this message is incorrect as of writing. It should read:

(Word) Cookie
(Dword) Result

Note the official client always sends zero for the cookie in the delete request but the value replied here is whatever you actually send. Conforming clients at least for 1.14 should send zero for the cookie.

Kaylin
🔎

(Word) Unknown is actually (Word) Cookie. The official client always seems to send 0 for this value but any value you send here is echoed back in the reply. Note as of writing this comment the reply does not have this field documented. See < https://bnetdocs.org/packet/411/mcp-chardelete > for more information.

Kaylin
🔎

In order to shed some light on any potential confusion regarding the modern format of this message here have been my findings with regards to the formatting. These messages are encapsulated by the MCP protocol so they all have their 3-byte MCP protocol header. The ladder data message always also contains a 7 byte header present in all the ladder data messages. It is as follows:

(Byte) Ladder Type
(Word) Total Size
(Word) Chunk Size
(Word) Data Index

The rest of the message is then:

(void) Data Payload

The Ladder type is the type of ladder. Possible values for this byte are given in the request message documentation here: < https://bnetdocs.org/packet/99/mcp-requestladderdata > The full data payload may be split across multiple of these messages and each message contains this 7 byte header above. The ultimate length of the reassembled payload is Total Size. The Chunk Size is the the size of the chunk of data payload included in this individual message. The Data Index is where to insert this payload chunk into the byte sequence to reassemble the stream.

The Data payload in its entirety is then as follows. There seem to be no short messages at least anymore; no weird length calculations. The Reassembled payload looks like this:

(Dword) first entry rank
(Dword) actual number of entries
(Dword) unknown (always 16)
struct {
(Dword) Experience
(Dword) Zero (may be high dword of exp)
(Byte) Character Flags (Dead, sorceress, etc)
(Byte) Number of Completed Acts
(Word) Level
(Byte) Name[16]
}[actual number of entries]

I say actual number of entries because this varies but it never seems to be more than 16. The (Dword) unknown may in fact be the maximum number of entries returned. I note (Qword) Experience described above is probably two dwords since Diablo II is a 32-bit program. Name is a fixed length 16-byte field containing a null terminated character string followed by enough bytes to ensure the total length of that field is 16 bytes. Remember this is the reassembled payload! This data may be split into multiples of the MCP_REQUESTLADDERDATA reply and may break in the middle of data fields. Once reassembled it seems this structure has no weird length fields, short strings, or sometimes missing headers. My tests overnight indicated the multiple payload messages were received in order but you might not wish to bank on this being the case.

Kaylin
🔎

byte unused is actually string game_desc. You'll get the empty string if empty. In addition the terminating message that terminates the gamelist seems to be an additional byte in length, I'm not sure if it's always an additional byte in length. In retrospect this should have been obvious to me as the client gets the game description from this message too.

Kaylin
🔎

The game status field indicates the game type and difficulty associated with the game. The following return values were observed consistently on both USEast and USWest realms.

Non-Ladder Softcore Expansion (Hell)     : 0x00102004
Non-Ladder Softcore Expansion (Nightmare): 0x00101004
Non-Ladder Softcore Expansion (Normal)   : 0x00100004

Non-Ladder Hardcore Expansion (Hell)     : 0x00102804
Non-Ladder Hardcore Expansion (Nightmare): 0x00101804
Non-Ladder Hardcore Expansion (Normal)   : 0x00100804

Non-Ladder Softcore Classic   (Hell)     : 0x00002004
Non-Ladder Softcore Classic   (Nightmare): 0x00001004
Non-Ladder Softcore Classic   (Normal)   : 0x00000004

Non-Ladder Hardcore Classic   (Hell)     : 0x00002804
Non-Ladder Hardcore Classic   (Nightmare): 0x00001804
Non-Ladder Hardcore Classic   (Normal)   : 0x00000804

Ladder     Softcore Expansion (Hell)     : 0x00302004
Ladder     Softcore Expansion (Nightmare): 0x00301004
Ladder     Softcore Expansion (Normal)   : 0x00300004

Ladder     Hardcore Expansion (Hell)     : 0x00302804
Ladder     Hardcore Expansion (Nightmare): 0x00301804
Ladder     Hardcore Expansion (Normal)   : 0x00300804

Ladder     Softcore Classic   (Hell)     : 0x00202004
Ladder     Softcore Classic   (Nightmare): 0x00201004
Ladder     Softcore Classic   (Normal)   : 0x00200004

Ladder     Hardcore Classic   (Hell)     : 0x00202804
Ladder     Hardcore Classic   (Nightmare): 0x00201804
Ladder     Hardcore Classic   (Normal)   : 0x00200804

In order to decode a successful response interpret it in the following way:

The game difficulty is this one of:

Nightmare : 0x1000;
Hell      : 0x2000;
Normal    : 0;

The game type is the bitwise or of any of:

Ladder    : 0x200000;
Expansion : 0x100000;
Hardcore  : 0x800;

bitwise or the difficulty with the game type and the number 0x4 to get a successful status response.

Other than the documented 0 response above, the server also returns 0xFFFFFFFE indicating the request was not valid for a number of reasons. This was observed sending the empty string, sending characters disallowed in a game name, and sending a string of a game that did not exist. In no case did this cause me to be IP-banned. There appears to be no differentiation between passworded and public games.

The field unknown appears to be the level restriction. It should be interpreted as 2 bytes not one word. The first byte read off the wire is the level to start from. On game creation this appears to be the creator's level but it can change as the creator gains levels or leaves the game. The second byte should be interpreted as the level difference. e.g. creating a game with a level 98 character with level restriction set to 30 results in 0x62 0x1E initially (again, this can change). To find the level restriction displayed in-game take the range to be max(1, level-difference) to min(99, level+difference). Thus a difference of 30 for a 98 character reads as 68 to 99 in-game. If there is no restriction specified these fields are both zero.

There is no restriction on requesting game information. A character need not be of the correct type, have the game password, have completed the correct difficulty, nor meet the level restriction in order to request game information.