Khan Online: Server emulator notes
  1. 1. Initialization
  2. 2. References
    1. 2.1. GameData.bin
    2. 2.2. Packets
      1. 2.2.1. Outgoing
      2. 2.2.2. Initial packet ($00)
      3. 2.2.3. Login attempt ($F0)
      4. 2.2.4. Incoming
      5. 2.2.5. First packet ($65)
      6. 2.2.6. Server list ($03E9)
      7. 2.2.7. Subserver list ($03EA)
    3. 2.3. Game states

Someone has asked me to create a server emulator for this Mirinae’s MMORPG, shut down in 2008. I have done some preliminary work but the person has lost interest in this project so I’m posting my notes here in case they help somebody else.

All work was done on the client version $12C (dec. 300, which probably stands for 3.0) – taken from the version field of the first packet. KhanClient.exe info for the reference:

Size:   3 219 456 bytes
MD5:    48ab3adcac223bcf49597263fa587171
CRC32:  9c12d4af

Data structures below follow Lightpath notation and use the following common contexts:

lp; 8-bit value.
(byte) % ANY
; 16-bit value.
(word) % ANY ANY
; 32-bit value.
(dword) % ANY ANY ANY ANY
; null-terminated string.
(strz) % [^\0]* \0

Numbers prefixed with dollar symbol ($) are in hexadecimal notation, i.e. $40 equals 0x40 (dec. 64).

Note: I do not have the sources of the server emulator because I did not complete it. This is a huge and expensive work so no use in posting a comment asking me to send the emulator to you.

Initialization

First of all the game creates at least two windows, one of which is hidden control window for WinSock events which HWND is placed into var 018501D8. Then the game reads and overwrites gamedata.bin (fn 004DF640). First 6 bytes are used to send the initial packet. Then it establishes socket connection to the main server.

This is done (fn 004DF6F0) by looking up hardcoded host name (var 006A4108) which can also be an IP (both arguments are acceptable for fn 004DE670). Port number (2112) is hardcoded into the function’s body. The game listens for socket’s FD_READ and FD_CLOSE events that are sent to its hidden window (fn 004DE370, window proc fn 004DE2A0). It also sets up a timer and checks for new data to receive each 30 seconds (? fn 004DF110).

When FD_READ occurs the game accumulates at least 8 bytes of received data before it processes them in any way. Likewise, it waits until the full packet is received by checking its length (word prepended to data). Received buffers may contain more than one packet, if so they are concatenated together like [word] length [length] data [w] pct2_length .... Once read they are broken down and put in queue for later processing.

Initial packet is sent by fn 004DF730, 8 bytes (see the reference for its structure).

Initial packets received have this structure (all in one read; length prefixes omitted here and below):

lp; first - server & subserver list
. "type" x 65 00
; ... - see the reference

; second - server list
. "type" x E9 03
; ... - see the reference

; third - subserver list
. "type" x EA 03
; ... - see the reference

References

GameData.bin

lp. word "runCounter"
. word "unk1"
. word "unk2"
. dword "unused"

This file is always 10 bytes long. If runCounter is >= 35 the game will set this field to 35 and fully overwrite gamedata.bin (only this word changes). It sems to track new installations so the server knows how many times the client has been ran during the first 35 runs.

Last 4 bytes seem to be unused.

Packets

Every packet being sent and received gets prepended with a word field indicating new packet length (i.e. old length + 2). This way initial packet being sent becomes 10 bytes long.

Additionally:

Outgoing

Received packet types scattered around the code (3rd argument to fn 004DEF90):

$00
Initial packet – the first one upon connect
$F0
Login attempt (username and password)

Packets except $00 are crypted (fn 0060F7B0):

lp; (unencrypted) regular length field
. word "length"
. (unencrypted) byte "type"
; this value depends on a dynamic var 0184E7F8
. (unencrypted) byte "salt"
; encrypted
. "data" =
  ; ...
; calculated for decoded buffer and resembles CRC by fixed table
. dword "checksum"

Initial packet ($00)

lp; usual header for every packet
. word "packetLength"
; a hardcoded var 0184DAA0, presumably client version
. word "version"
; fields from gamedata.bin
. word "counter"
. word "unk1"
. word "unk2"

Login attempt ($F0)

lp; hardcoded value
. word "unknown" x 01 00
; fields below have hardcoded limits; size of payload is 50 bytes
; but it grows up to 58 as the packet being sent
. "login"
  CYCLE "16"
. "password"
  CYCLE "32"

Incoming

Received packet types as recognized by fn 00415E00:

$65
List of servers & subservers (first connection response) – payload seem to be unsued at all. This data is duplicated in $03E9 and $03EA
$64
??
$01
??
$0B
??
$0C
??
$03E9
List of servers; maximum 100
$03EA
List of subservers
$03EC
??

Other packets are ignored. A packet might change current game state:

$65
$40
$64
$44 or unchanged
$01
Some other or unchanged
$0B
Some other or unchanged
$0C
Some other or unchanged
$03E9
Unchanged
$03EA
$42
$03EC
$44

If an empty packet (length field 0) was received command ID can be $44 or unchanged. «Unchanged» means that current game state is preserved.

Game states below $40 take part in repeating processing each 30 sec by fn 0040CFD0.

First packet ($65)

lp; total subservers
. byte "count"
. CYCLE =
  "subserver" =
    . dword "ip"
    . word "port"
    ; lower value means less people online
    . byte "loadValue?"
    . strz "name"

Server list ($03E9)

lp; max 100 (hardcoded limit)
. word "count"
. CYCLE =
  "server" =
    . strz "title"
    ; can be empty (end on 0x00)
    . strz "description?"

Subserver list ($03EA)

lp. word "count"
. CYCLE =
  "subserver" =
    ; 1-based.
    . byte "serverIndex"
    ; 1-based.
    . byte "subserverIndex"
    ; proper LE order
    . dword "ip"
    ; proper LE order
    . word "port?"
    ; 0/1
    . byte "isPvP"
    ; same as in the first packet
    . byte "loadValue?"

Game states

The end of WinMain contains message loop (fn 0046A357) that in addition to regular PeekMessage - TranslateMessage - DispatchMessage streamline also does PeekMessage and if there are no messages in the queue does some «diel action» according to current game state. This action is done by a procedure (fn 00409880) with a big switch on state IDs from $32 to $FA (dec. 250). Initial state for the game is $32.

Known states and their IDs:

$32
Game initialization: graphics, save directory, etc. When done sets command to $3C.
$3C
Allocates some memory and sets command to $3E.
$3E
Fetches next received server packet, if available. Is ran in cycle. Depending on the packet type may switch next command to something else, or remain the same.
$40
Identical to $3E.
$42
Sets command to $46 if $3E has not yet finished initializing or to $5A otherwise.
$46
Initializes audio (MP3) subsystem. Sets command to $48.