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.
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
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.
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.
Received packet types scattered around the code (3rd argument to fn 004DEF90):
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"
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"
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"
Received packet types as recognized by fn 00415E00:
Other packets are ignored. A packet might change current game state:
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.
lp; total subservers . byte "count" . CYCLE = "subserver" = . dword "ip" . word "port" ; lower value means less people online . byte "loadValue?" . strz "name"
lp; max 100 (hardcoded limit) . word "count" . CYCLE = "server" = . strz "title" ; can be empty (end on 0x00) . strz "description?"
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?"
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.