As with a lot of my projects, this is a developer's project. Most people will not have a need for anything here. There aren't any screenshots due to the nature of this project, but I have made a video of the concepts in action if you are interested:
There are 3 main groups of projects included in this collection of tools. The first group are the simple tools to compress and decompress files in a format Silkroad.exe uses. The second group of tools implements a patch file downloader from official server versions to test the tools. The third group of tools implements a simple DownloadServer to show the basics.
In the first group, there are 5 projects:
CompressToLZMA - Compresses an input file into a LZMA compressed file Silkroad.exe can process.
CompressToZLIB - Compresses an input file into a ZLIB compressed file Silkroad.exe can process.
DecompressFromLZMA -Decompresses an input file from LZMA into the original file.
DecompressFromZLIB - Decompresses an input file from ZLIB into the original file.
BatchProcess - Wraps using all 4 tools into one utility while providing some extra features to help manage folders of files.
This set of tools implements the basic tools for allowing people to compress and decompress files into a format Silkroad.exe can process. The reason there are two formats is because ISRO updated the format while most of the other versions did not. This probably was due to the larger player base ISRO has and Joymax wanted better compression in their files. When RSRO bought their license, they happened to get the ISRO code so they too use lzma. CSRO and KSRO still use the ZLIB version and I'd imagine JSRO, VSRO, TSRO do as well, but I've not confirmed.
There is nothing special with the format of these files except there is an extra DWORD at the start of the file that tells the original file size. Once that is processed, the remaining data is simply the output stream from the respective compression used. As a result of this simplicity, compressing your own files into the format is equally as trivial. The generated files won't match the originals since different compression options are used, but in the end, the original data is the same.
In fact, once you look over the code required, you'd probably ask why this hasn't been done sooner! It's a lot simpler than I had though, but it still took a bit of time of reversing and tediously testing code to make the connections shown in the implementations. I started out with using direct asm code from the client, then moved into lower level SDK functions, then finally worked my way to the higher level function that greatly simplifies the amount of work to be done.
While I have my files coded to specifically process files with .lzma and .zlib extensions, this can be changed to any arbitrary extensions. In doing so, you can use the ZLIB versions to easily decompress the DownloadServer files from sl.rar (leaked JSRO files) if you wanted them. To do it by hand, simply add a ".zlib" extension to the end of any file and drag it on top of the existing precompiled DecompressFromZLIB.exe tool. I will also mention that people have already done this though, so it is nothing new (ZLIB wise).
I'll go ahead and paste the main CPP files for people to get an idea of how simple the code is if you don't want to download the entire package (sorry about the size, I have sqlite3 in my Common folder)
CompressToLZMA
Code:
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <vector> #include <string> #include "../Common/lzma/LzmaUtil/Lzma86Enc.h" #include "../Common/lzma/LzmaUtil/Lzma86Dec.h" int main(int argc, char * argv[]) { if(argc < 2) { printf("Usage:\n\tCompress <input file> ... [input file]\n"); return -1; } for(int x = 1; x < argc; ++x) { std::vector<unsigned char> input_data; unsigned char * input_data_ptr = 0; SizeT input_data_size = 0; std::vector<unsigned char> output_data; unsigned char * output_data_ptr = 0; SizeT output_data_size = 0; FILE * infile = 0; FILE * outfile = 0; UInt64 unpackSize = 0; SRes result = 0; errno_t err = 0; size_t ind = 0; std::string ifn; std::string ofn; ofn = argv[x]; ifn = argv[x]; if((err = fopen_s(&infile, argv[x], "rb")) || infile == NULL) { printf("Error: Could not open input file [%s] due to fopen_s error (%i).\n", argv[x], err); continue; } fseek(infile, 0, SEEK_END); input_data_size = ftell(infile); input_data.resize(input_data_size + 1); // + 1 so [0] is always valid input_data_ptr = &input_data[0]; fseek(infile, 0, SEEK_SET); fread(&input_data[0], 1, input_data.size(), infile); fclose(infile); output_data_size = static_cast<SizeT>(1.25 * input_data_size); // Add 25% extra space if(output_data_size < 4096) // Make sure smaller files have enough buffer space { output_data_size = 4096; } output_data.resize(output_data_size + 1); // + 1 so [0] is always valid output_data_ptr = &output_data[0]; result = Lzma86_Encode(output_data_ptr, &output_data_size, input_data_ptr, input_data_size, 9, 0x02000000, SZ_FILTER_NO); switch(result) { case SZ_OK: { ofn += ".lzma"; if((err = fopen_s(&outfile, ofn.c_str(), "wb")) || outfile == NULL) { printf("Error: Could not save the output file [%s] for input file [%s] due to fopen_s error (%i).\n", ofn.c_str(), argv[x], err); } else { result = Lzma86_GetUnpackSize(output_data_ptr, output_data_size, &unpackSize); fwrite(&unpackSize, 1, 4, outfile); fwrite(output_data_ptr, 1, output_data_size, outfile); fclose(outfile); //printf("Information: %s was successfully compressed!\n", argv[x]); } } break; case SZ_ERROR_THREAD: printf("Error: SZ_ERROR_THREAD detected for input file [%s].\n", argv[x]); break; case SZ_ERROR_MEM: printf("Error: SZ_ERROR_MEM detected for input file [%s].\n", argv[x]); break; case SZ_ERROR_PARAM: printf("Error: SZ_ERROR_PARAM detected for input file [%s].\n", argv[x]); break; case SZ_ERROR_OUTPUT_EOF: printf("Error: SZ_ERROR_OUTPUT_EOF detected for input file [%s].\n", argv[x]); break; default: printf("Error: %i detected for input file [%s].\n", result, argv[x]); break; } } return 0; }
Code:
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <vector> #include <string> #include "../Common/zlib/zlib.h" int main(int argc, char * argv[]) { if(argc < 2) { printf("Usage:\n\tCompress <input file> ... [input file]\n"); return -1; } for(int x = 1; x < argc; ++x) { std::vector<unsigned char> input_data; unsigned char * input_data_ptr = 0; DWORD input_data_size = 0; std::vector<unsigned char> output_data; unsigned char * output_data_ptr = 0; DWORD output_data_size = 0; FILE * infile = 0; FILE * outfile = 0; int result = 0; errno_t err = 0; size_t ind = 0; std::string ifn; std::string ofn; ofn = argv[x]; ifn = argv[x]; if((err = fopen_s(&infile, argv[x], "rb")) || infile == NULL) { printf("Error: Could not open input file [%s] due to fopen_s error (%i).\n", argv[x], err); continue; } fseek(infile, 0, SEEK_END); input_data_size = ftell(infile); input_data.resize(input_data_size + 1); // + 1 so [0] is always valid input_data_ptr = &input_data[0]; fseek(infile, 0, SEEK_SET); fread(&input_data[0], 1, input_data.size(), infile); fclose(infile); output_data_size = static_cast<DWORD>(1.25 * input_data_size); // Add 25% extra space if(output_data_size < 4096) // Make sure smaller files have enough buffer space { output_data_size = 4096; } output_data.resize(output_data_size); output_data_ptr = &output_data[0]; result = compress(output_data_ptr, &output_data_size, input_data_ptr, input_data_size); switch(result) { case Z_OK: { ofn += ".zlib"; if((err = fopen_s(&outfile, ofn.c_str(), "wb")) || outfile == NULL) { printf("Error: Could not save the output file [%s] for input file [%s] due to fopen_s error (%i).\n", ofn.c_str(), argv[x], err); } else { fwrite(&input_data_size, 1, 4, outfile); fwrite(output_data_ptr, 1, output_data_size, outfile); fclose(outfile); //printf("Information: %s was successfully compressed!\n", argv[x]); } } break; case Z_MEM_ERROR: printf("Error: Z_MEM_ERROR detected for input file [%s].\n", argv[x]); break; case Z_BUF_ERROR: printf("Error: Z_BUF_ERROR detected for input file [%s].\n", argv[x]); break; default: printf("Error: %i detected for input file [%s].\n", result, argv[x]); break; } } return 0; }
Code:
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <vector> #include <string> #include "../Common/lzma/LzmaUtil/Lzma86Dec.h" int main(int argc, char * argv[]) { if(argc < 2) { printf("Usage:\n\tDecompress <input file> ... [input file]\n"); return -1; } for(int x = 1; x < argc; ++x) { std::vector<unsigned char> input_data; unsigned char * input_data_ptr = 0; SizeT input_data_size = 0; std::vector<unsigned char> output_data; unsigned char * output_data_ptr = 0; SizeT output_data_size = 0; FILE * infile = 0; FILE * outfile = 0; UInt64 unpackSize = 0; SRes result = 0; errno_t err = 0; size_t ind = 0; std::string ifn; std::string ofn; ofn = argv[x]; ifn = argv[x]; ind = ifn.find(".lzma"); if(ind != ifn.size() - 5) { printf("Error: The input file should have a trailing extension of .lzma [%s]\n", argv[x]); continue; } if((err = fopen_s(&infile, argv[x], "rb")) || infile == NULL) { printf("Error: Could not open input file [%s] due to fopen_s error (%i).\n", argv[x], err); continue; } fseek(infile, 0, SEEK_END); input_data_size = ftell(infile); input_data.resize(input_data_size + 1); // + 1 so [0] is always valid input_data_ptr = &input_data[0]; fseek(infile, 0, SEEK_SET); fread(&input_data[0], 1, input_data.size(), infile); fclose(infile); infile = 0; result = Lzma86_GetUnpackSize(input_data_ptr + 4, input_data_size - 4, &unpackSize); output_data_size = static_cast<DWORD>(unpackSize); if(output_data_size != *(SizeT *)input_data_ptr) { printf("Error: Unpack Size Mismatch (%ul) vs (%ul) for [%s]\n", output_data_size, *(SizeT *)input_data_ptr, argv[x]); continue; } output_data.resize(output_data_size + 1); // + 1 so [0] is always valid output_data_ptr = &output_data[0]; input_data_ptr += 0x04; input_data_size -= 0x04; result = Lzma86_Decode(output_data_ptr, &output_data_size, input_data_ptr, &input_data_size); switch(result) { case SZ_OK: { ind = ofn.find(".lzma"); if(ind != ofn.size() - 5) { printf("Error: Could not generate the name of the output file for [%s]\n", argv[x]); continue; } ofn = ofn.substr(0, ind); if((err = fopen_s(&outfile, ofn.c_str(), "wb")) || outfile == NULL) { printf("Error: Could not save the output file [%s] for input file [%s] due to fopen_s error (%i).\n", ofn.c_str(), argv[x], err); } else { fwrite(output_data_ptr, 1, output_data_size, outfile); fclose(outfile); //printf("Information: %s was successfully decompressed!\n", argv[x]); } } break; case SZ_ERROR_DATA: printf("Error: SZ_ERROR_DATA detected for input file [%s].\n", argv[x]); break; case SZ_ERROR_MEM: printf("Error: SZ_ERROR_MEM detected for input file [%s].\n", argv[x]); break; case SZ_ERROR_UNSUPPORTED: printf("Error: SZ_ERROR_UNSUPPORTED detected for input file [%s].\n", argv[x]); break; case SZ_ERROR_INPUT_EOF: printf("Error: SZ_ERROR_INPUT_EOF detected for input file [%s].\n", argv[x]); break; default: printf("Error: %i detected for input file [%s].\n", result, argv[x]); break; } } return 0; }
Code:
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <vector> #include <string> #include "../Common/zlib/zlib.h" int main(int argc, char * argv[]) { if(argc < 2) { printf("Usage:\n\tDecompress <input file> ... [input file]\n"); return -1; } for(int x = 1; x < argc; ++x) { std::vector<unsigned char> input_data; unsigned char * input_data_ptr = 0; DWORD input_data_size = 0; std::vector<unsigned char> output_data; unsigned char * output_data_ptr = 0; DWORD output_data_size = 0; FILE * infile = 0; FILE * outfile = 0; int result = 0; errno_t err = 0; size_t ind = 0; std::string ifn; std::string ofn; ofn = argv[x]; ifn = argv[x]; ind = ifn.find(".zlib"); if(ind != ifn.size() - 5) { printf("Error: The input file should have a trailing extension of .zlib [%s]\n", argv[x]); continue; } if((err = fopen_s(&infile, argv[x], "rb")) || infile == NULL) { printf("Error: Could not open input file [%s] due to fopen_s error (%i).\n", argv[x], err); continue; } fseek(infile, 0, SEEK_END); input_data_size = ftell(infile); input_data.resize(input_data_size + 1); // + 1 so [0] is always valid input_data_ptr = &input_data[0]; fseek(infile, 0, SEEK_SET); fread(&input_data[0], 1, input_data.size(), infile); fclose(infile); infile = 0; output_data_size = *(DWORD *)input_data_ptr; if(output_data_size < 4096) { output_data_size = 4096; } output_data.resize(output_data_size); output_data_ptr = &output_data[0]; input_data_ptr += 0x04; input_data_size -= 0x04; result = uncompress(output_data_ptr, &output_data_size, input_data_ptr, input_data_size); switch(result) { case Z_OK: { ind = ofn.find(".zlib"); if(ind != ofn.size() - 5) { printf("Error: Could not generate the name of the output file for [%s]\n", argv[x]); continue; } ofn = ofn.substr(0, ind); if((err = fopen_s(&outfile, ofn.c_str(), "wb")) || outfile == NULL) { printf("Error: Could not save the output file [%s] for input file [%s] due to fopen_s error (%i).\n", ofn.c_str(), argv[x], err); } else { fwrite(output_data_ptr, 1, output_data_size, outfile); fclose(outfile); //printf("Information: %s was successfully decompressed!\n", argv[x]); } } break; case Z_MEM_ERROR: printf("Error: Z_MEM_ERROR detected for input file [%s].\n", argv[x]); break; case Z_BUF_ERROR: printf("Error: Z_BUF_ERROR detected for input file [%s].\n", argv[x]); break; case Z_DATA_ERROR: printf("Error: Z_DATA_ERROR detected for input file [%s].\n", argv[x]); break; default: printf("Error: %i detected for input file [%s].\n", result, argv[x]); break; } } return 0; }
In the second group, there is 1 project:
SilkroadPatchDownloader - Implements a simple clientless that will intentionally trigger patch updates so the compressed files can be saved to disk.
I wrote this project so users will be able to download patch files from the servers and save them to disk. In addition, the code shows how to process these packets correctly so if someone wanted to emulate how Silkroad.exe processes patch packets, they can refer to the code as well.
Inside the /bin folder, there are a few batch files with a name beginning with 1_. These batch files start the SilkroadPatchDownloader.exe configured for specific Silkroad versions. Since there is a disconnect between the versions and which compression method they use, this has to be specified to the program. After you run one of the files, the patch files are downloaded into a folder named output/<locale>/<version>. A sample bat for isro is: "SilkroadPatchDownloader.exe lzma gwgt1.joymax.com 15779 18 254 > isro.log". 18 is the locale, 254 is the version to send, lzma is the compression method and then the standard GatewayServer address. I pipe the output to a file using >.
Once you have files downloaded, either zlib or lzma files, you can then drag them on top of the DecompressFromZLIB.exe or DecompressFromLZMA.exe programs to get the original files back. Alternatively, the batch files that begin with a 2_ name do this. They show how to decompress a folder of files from the lzma or zlib format. Simply right click and choose edit on these batch files to see the format required. In addition, check out the code itself for the programs as you go along.
The batch files with 3_ in their name show how to compress your files into a format ready for use by a DownloadServer. The organization of the input files is very deliberate in setting up a system where automating new patches is a lot easier. For an emu project, it's up to the coder to work out a system that works best for them though, my system is really basic to show the main concepts.
Here's the code for the PatchDownloader:
Code:
#define _CRT_SECURE_NO_WARNINGS #include "../Common/Network.h" #include "../Common/StreamUtility.h" #include "../Common/SilkroadSecurity.h" #include "../Common/DumpToString.h" #include "../Common/RecursiveCreateDirectory.h" #include <windows.h> #include <conio.h> #include <map> #include <list> #include <string> #include <sstream> #include <boost/filesystem.hpp> namespace fs = boost::filesystem; class SilkroadPatchDownloader : private Network { private: struct BigPacket { WORD opcode; WORD count; std::vector<char> data; }; struct FileEntry { BYTE m_inpk2; DWORD m_id; DWORD m_filesize; std::string m_filename; std::string m_filepath; std::vector<char> m_data; FileEntry(DWORD id, const std::string & filename, std::string & filepath, DWORD filesize, BYTE inpk2) : m_id(id), m_filesize(filesize), m_filename(filename), m_filepath(filepath), m_inpk2(inpk2) { } }; struct ClientContext { int m_sid; SilkroadSecurity m_security; std::vector<char> m_packet; BigPacket m_big_packet; bool m_ping; DWORD m_last_action; std::list<FileEntry> m_files; int m_type; ClientContext(int sid) : m_sid(sid) { m_packet.reserve(8192); m_packet.resize(8192); m_packet.clear(); m_ping = false; m_last_action = 0; m_type = 0; } }; std::map<int, ClientContext *> m_current_clients; std::map<int, std::list<FileEntry>> m_client_files; std::map<int, int> m_key_to_type; HANDLE m_exit_event; fs::path m_full_path; std::string m_patch_host; unsigned short m_patch_port; BYTE m_patch_locale; DWORD m_patch_version; DWORD m_real_version; std::string m_ext; private: void OnSessionStart(int sid, const char * const remoteHost, unsigned short remotePort, const char * const localHost, unsigned short localPort, int key) { printf("[%s][%s:%i][%s:%i]\n", __FUNCTION__, remoteHost, remotePort, localHost, localPort); if(sid == 0) { printf("Error: Could not connect to the server.\n"); SetEvent(m_exit_event); } else if(key == -1) { ClientContext * client_context = new ClientContext(sid); client_context->m_security.SetSecurityMode(0x0E); m_current_clients.insert(std::make_pair(sid, client_context)); client_context->m_type = 0; Network::PostRead(sid, client_context->m_security.GetNextReadSize(0, 0), true); } else { std::map<int, int>::iterator itr0 = m_key_to_type.find(key); if(itr0 == m_key_to_type.end()) { PostError(sid, -1, "Invalid key"); return; } int client_type = itr0->second; m_key_to_type.erase(itr0); if(client_type == 2) { std::map<int, std::list<FileEntry>>::iterator itr1 = m_client_files.find(key); if(itr1 == m_client_files.end()) { PostError(sid, -1, "Invalid key"); return; } ClientContext * client_context = new ClientContext(sid); client_context->m_security.SetSecurityMode(0x0E); client_context->m_files = itr1->second; client_context->m_type = client_type; m_client_files.erase(itr1); m_current_clients.insert(std::make_pair(sid, client_context)); Network::PostRead(sid, client_context->m_security.GetNextReadSize(0, 0), true); } else { PostError(sid, -1, "Invalid client type (%i)", client_type); } } } void OnSessionRead(int sid, const char * const buffer, int size) { std::map<int, ClientContext *>::iterator itr = m_current_clients.find(sid); if(itr == m_current_clients.end()) { PostError(sid, -1, "Unknown sid (%i)\n", sid); return; } ClientContext * client_context = itr->second; std::copy(buffer, buffer + size, std::back_inserter(client_context->m_packet)); size_t readSize = client_context->m_security.GetNextReadSize(&client_context->m_packet[0], client_context->m_packet.size()); if(readSize) { Network::PostRead(sid, readSize, true); return; } size_t readCount = client_context->m_security.GetReadProcessCount(&client_context->m_packet[0], client_context->m_packet.size()); if(readCount == 0) { Network::PostError(sid, -1, "GetReadProcessCount returned 0."); return; } std::pair<int, std::vector<char>> result = client_context->m_security.ExtractPacket(&client_context->m_packet[0], client_context->m_packet.size()); client_context->m_packet.clear(); if(result.first < 0) { Network::PostError(sid, -1, "ExtractPacket returned an error (%i).", result.first); return; } HandlePacket(client_context, result.second, (result.first == 1)); Network::PostRead(sid, client_context->m_security.GetNextReadSize(0, 0), true); } void OnSessionWrite(int sid, const char * const buffer, int size) { } void OnSessionError(int sid, int error, const char * msg) { printf("%s(%i, %i, %s)\n", __FUNCTION__, sid, error, strlen(msg) ? msg : "\"\""); std::map<int, ClientContext *>::iterator itr = m_current_clients.find(sid); if(itr != m_current_clients.end()) { delete itr->second; m_current_clients.erase(itr); } } void OnPoll() { std::map<int, ClientContext *>::iterator itr = m_current_clients.begin(); while(itr != m_current_clients.end()) { ClientContext * client_context = itr->second; if(client_context->m_ping) { if(GetTickCount() - client_context->m_last_action >= 5000) { StreamBuilder response; SendPacket(client_context, 0x2002, response, false); } } ++itr; } } void OnShutdown() { } void OnLog(const char * const file, int line, const char * const func, const char * const text) { printf("%s(%s, %i, %s, %s)\n", __FUNCTION__, file, line, func, text); } private: void SendPacket(ClientContext * client_context, WORD opcode, StreamBuilder & data, bool encrypted) { StreamBuilder builder; builder.Write<WORD>(data.GetCount()); builder.Write<WORD>(opcode); builder.Write<WORD>(0); builder.WriteArray<char>(data.GetStream(), data.GetCount()); Send(client_context, builder.ToVector(), encrypted); } void SendPacket(ClientContext * client_context, WORD opcode, std::vector<char> & data, bool encrypted) { StreamBuilder builder; builder.Write<WORD>(data.size()); builder.Write<WORD>(opcode); builder.Write<WORD>(0); builder.WriteArray<char>(&data[0], data.size()); Send(client_context, builder.ToVector(), encrypted); } void Send(ClientContext * client_context, std::vector<char> output, bool encrypted) { //printf("[C->S][%i][%.4X][%i bytes]%s\n", client_context->m_sid, *(unsigned short *)&output[2], *(unsigned short *)&output[0], encrypted ? "[Enc]" : ""); //printf("%s\n\n", DumpToString(&output[0] + 6, output.size() - 6).c_str()); output = client_context->m_security.FormatC2SPacket(&output[0], output.size(), encrypted); PostWrite(client_context->m_sid, &output[0], output.size()); client_context->m_last_action = GetTickCount(); } bool HandlePacket(ClientContext * client_context, std::vector<char> & packet, bool encrypted) { StreamReader reader(&packet[0], packet.size()); WORD size = reader.Read<WORD>(); WORD opcode = reader.Read<WORD>(); WORD security = reader.Read<WORD>(); //printf("[S->C][%i][%.4X][%i bytes]\n", client_context->m_sid, opcode, size); //printf("%s\n\n", DumpToString(reader.GetCurStream(), reader.GetCurCount()).c_str()); if(opcode == 0x5000) { if(client_context->m_security.Handshake(&packet[0], packet.size()) == false) { Network::PostError(client_context->m_sid, 1, "Handshake returned false."); return false; } packet = client_context->m_security.BuildHandshakePacket(); Send(client_context, packet, false); if(*(unsigned short *)(&packet[2]) == 0x9000) { StreamBuilder response; response.Write<WORD>(strlen("SR_Client")); response.WriteArray<char>("SR_Client", strlen("SR_Client")); response.Write<BYTE>(0); SendPacket(client_context, 0x2001, response, true); } } else if(opcode == 0x2001) { WORD len = 0; std::string name; BYTE flag; len = reader.Read<WORD>(); name.resize(len); reader.ReadArray(&name[0], len); flag = reader.Read<BYTE>(); if(name == "GatewayServer") { StreamBuilder response; response.Write<BYTE>(m_patch_locale); response.Write<WORD>(strlen("SR_Client")); response.WriteArray<char>("SR_Client", strlen("SR_Client")); response.Write<DWORD>(m_patch_version); SendPacket(client_context, 0x6100, response, true); } else if(name == "DownloadServer") { // Nothing to do here // } } else if(opcode == 0x600D) { BYTE type = reader.Read<BYTE>(); if(type == 1) // Header { client_context->m_big_packet.count = reader.Read<WORD>(); // how many 0x600D data segments client_context->m_big_packet.opcode = reader.Read<WORD>(); // opcode of the spanned packet client_context->m_big_packet.data.clear(); client_context->m_big_packet.data.resize(6, 0); } else if(type == 0) // Data { std::copy(reader.GetCurStream(), reader.GetCurStream() + reader.GetCurCount(), std::back_inserter(client_context->m_big_packet.data)); client_context->m_big_packet.count--; if(client_context->m_big_packet.count == 0) { *(unsigned short *)&client_context->m_big_packet.data[0] = client_context->m_big_packet.data.size() - 6; // This overflows on packets > 65k, check data size in packet handlers instead *(unsigned short *)&client_context->m_big_packet.data[2] = client_context->m_big_packet.opcode; HandlePacket(client_context, client_context->m_big_packet.data, false); } } } else if(opcode == 0xA100) { BYTE mode = reader.Read<BYTE>(); if(mode == 0x01) // All set { PostError(client_context->m_sid, -1, "Version is current"); SetEvent(m_exit_event); } else if(mode == 0x02) // Error { mode = reader.Read<BYTE>(); if(mode == 0x02) // Updates available { WORD len; std::string ip; WORD port; DWORD version; BYTE file; m_client_files.insert(std::make_pair(client_context->m_sid, std::list<FileEntry>())); std::list<FileEntry> & files = m_client_files[client_context->m_sid]; len = reader.Read<WORD>(); ip.resize(len); reader.ReadArray(&ip[0], len); // download server port = reader.Read<WORD>(); version = reader.Read<DWORD>(); m_real_version = version; file = reader.Read<BYTE>(); while(file) { DWORD id; DWORD filesize; std::string fn; std::string path; BYTE inpk2; id = reader.Read<DWORD>(); len = reader.Read<WORD>(); fn.resize(len); reader.ReadArray(&fn[0], len); // file name len = reader.Read<WORD>(); path.resize(len); reader.ReadArray(&path[0], len); // file path filesize = reader.Read<DWORD>(); inpk2 = reader.Read<BYTE>(); files.push_back(FileEntry(id, fn, path, filesize, inpk2)); file = reader.Read<BYTE>(); } m_key_to_type.insert(std::make_pair(client_context->m_sid, 2)); PostConnect(ip.c_str(), port, client_context->m_sid); PostError(client_context->m_sid, 0, "Now downloading updates"); } else if(mode == 0x04) // Server down { PostError(client_context->m_sid, -1, "The server is down"); SetEvent(m_exit_event); } else if(mode == 0x05) // Version too old { PostError(client_context->m_sid, -1, "Version is too old"); SetEvent(m_exit_event); } else if(mode == 0x01) // Version too new { PostError(client_context->m_sid, -1, "Version is too new"); SetEvent(m_exit_event); } else { PostError(client_context->m_sid, -1, "Unknown response (%i)", mode); SetEvent(m_exit_event); } } } else if(opcode == 0x1001) { std::copy(reader.GetCurStream(), reader.GetCurStream() + reader.GetCurCount(), std::back_inserter(client_context->m_files.front().m_data)); } else if(opcode == 0x6005) { client_context->m_ping = true; if(!client_context->m_files.empty()) { StreamBuilder response; response.Write<DWORD>(client_context->m_files.front().m_id); response.Write<DWORD>(0); SendPacket(client_context, 0x6004, response, false); } } else if(opcode == 0xA004) { FileEntry & fileEntry = client_context->m_files.front(); std::stringstream ss; ss << m_full_path.file_string() << "\\" << "output" << "\\" << (int)m_patch_locale << "\\" << m_real_version << "\\"; ss << fileEntry.m_filepath << "\\"; RecursiveCreateDirectoryA(ss.str().c_str(), NULL); std::string fpth = ss.str(); ss << fileEntry.m_filename; ss << "." << m_ext; fpth = ss.str(); FILE * of = 0; errno_t err; if((err = fopen_s(&of, fpth.c_str(), "wb")) || of == NULL) { printf("Error: Could not create output file [%s]\n", fpth.c_str()); } else { fwrite(&fileEntry.m_data[0], 1, fileEntry.m_filesize, of); fclose(of); printf("Information: Saved file [%s]\n", fileEntry.m_filename.c_str()); } client_context->m_files.erase(client_context->m_files.begin()); if(!client_context->m_files.empty()) { StreamBuilder response; response.Write<DWORD>(client_context->m_files.front().m_id); response.Write<DWORD>(0); SendPacket(client_context, 0x6004, response, false); } else { PostError(client_context->m_sid, 0, "Done downloading updates"); SetEvent(m_exit_event); } } return true; } public: SilkroadPatchDownloader() : m_full_path(fs::initial_path<fs::path>()) { m_exit_event = 0; m_patch_port = 0; m_patch_locale = 0; m_patch_version = 0; m_real_version = 0; } ~SilkroadPatchDownloader() { } int Run(int argc, char * argv[]) { if(argc != 6) { printf("Usage:\n\tSilkroadPatchDownloader <host> <port> <locale> <version> <lzma | zlib>\n"); return -1; } m_ext = argv[1]; m_patch_host = argv[2]; m_patch_port = atoi(argv[3]); m_patch_locale = atoi(argv[4]); m_patch_version = atoi(argv[5]); if(m_ext != "lzma" && m_ext != "zlib") { printf("Error: Invalid extension [%s]. Valid is \"lzma\" or \"zlib\" (without quotes).\n", m_ext.c_str()); return -1; } m_exit_event = CreateEvent(NULL, TRUE, FALSE, NULL); PostConnect(m_patch_host.c_str(), m_patch_port, -1); while(WaitForSingleObject(m_exit_event, 1) == WAIT_TIMEOUT) { Poll(); } Shutdown(); CloseHandle(m_exit_event); return 0; } }; int main(int argc, char * argv[]) { return SilkroadPatchDownloader().Run(argc, argv); }
DownloadServerDemo - Implements a simple ISRO DownloadServer that will send patches to Silkroad.exe when it connects if the version does not match.
This project is better suited for study rather than running. Make sure to watch the YouTube video linked above to see an older version in action. I'd advise users to make lots of backups of their PK2 files before trying to mess with this, as you can easily patch yourself into a situation where you can't recover easily. Also, each patch happens to change the version of the PK2, so the code is not setup to allow for trivial PK2 editing.
I have implemented a simple system so you can patch the version number back to any version you need through the use of a dummy file, but any changes you made are going to stay until you repatch them back, which makes backups a must. With that said, there is no precompiled file since it doesn't make sense due to the nature of what's being implemented. The code works, but it's more for showing how to implement the server rather than simply implementing one anyone can use as-is. I'd advise anyone interested in running the exe to read through the source in it's entirety to make all the necessary changes that might be needed between different versions.
Here's the code for the DownloadServerDemo:
Code:
#define _CRT_SECURE_NO_WARNINGS #include "../Common/Network.h" #include "../Common/StreamUtility.h" #include "../Common/SilkroadSecurity.h" #include "../Common/DumpToString.h" #include <windows.h> #include <stdio.h> #include <stdlib.h> #include <map> #include <list> #include <iostream> #include <conio.h> #include <boost/filesystem.hpp> namespace fs = boost::filesystem; // Set this version and recompile to test out patching between versions, // in a real server, this would be configured by a file. const DWORD current_version = 256; void recurse(const fs::path & p, std::list<std::string> & files) { fs::directory_iterator di(p); fs::directory_iterator de; while(di != de) { try { if(fs::is_directory(di->status())) { recurse(di->path(), files); } if(fs::is_regular_file(di->status())) { files.push_back(di->path().file_string()); } } catch(const std::exception & e) { std::cout << e.what() << std::endl; } ++di; } } void get_top_level_folders(const fs::path & p, std::list<std::string> & folders) { fs::directory_iterator di(p); fs::directory_iterator de; while(di != de) { try { if(fs::is_directory(di->status())) { folders.push_back(di->path().file_string()); } } catch(const std::exception & e) { std::cout << e.what() << std::endl; } ++di; } } class DownloadServer : private Network { private: struct ClientContext { int m_sid; SilkroadSecurity m_security; std::vector<char> m_packet; DWORD m_last_action; int m_key; ClientContext(int sid) : m_sid(sid) { m_packet.reserve(8192); m_packet.resize(8192); m_packet.clear(); m_last_action = 0; m_key = 0; } }; struct FileEntry { BYTE m_inpk2; DWORD m_id; DWORD m_filesize; std::string m_filename; std::string m_filepath; std::vector<char> m_data; FileEntry(DWORD id, const std::string & filename, std::string & filepath, DWORD filesize, BYTE unknown) : m_id(id), m_filesize(filesize), m_filename(filename), m_filepath(filepath), m_inpk2(unknown) { } FileEntry() : m_id(0), m_filesize(0), m_inpk2(0) { } }; std::map<int, ClientContext *> m_current_clients; std::map<int, FileEntry> m_server_files; std::map<int, std::vector<int>> m_version_files; int m_file_index; private: void OnSessionStart(int sid, const char * const remoteHost, unsigned short remotePort, const char * const localHost, unsigned short localPort, int key) { if(key == -1) { ClientContext * client_context = new ClientContext(sid); client_context->m_security.SetSecurityMode(0x0E); client_context->m_security.GenerateHandshake(); client_context->m_key = key; m_current_clients.insert(std::make_pair(sid, client_context)); Network::PostRead(sid, client_context->m_security.GetNextReadSize(0, 0), true); Send(client_context, client_context->m_security.BuildHandshakePacket(), false); } else if(key == -2) { ClientContext * client_context = new ClientContext(sid); client_context->m_security.SetSecurityMode(0x0E); client_context->m_security.GenerateHandshake(); client_context->m_key = key; m_current_clients.insert(std::make_pair(sid, client_context)); Network::PostRead(sid, client_context->m_security.GetNextReadSize(0, 0), true); Send(client_context, client_context->m_security.BuildHandshakePacket(), false); } else { PostError(sid, -1, "Invalid key (%i)\n", key); } } void OnSessionRead(int sid, const char * const buffer, int size) { std::map<int, ClientContext *>::iterator itr = m_current_clients.find(sid); if(itr == m_current_clients.end()) { PostError(sid, -1, "Unknown sid (%i)\n", sid); return; } ClientContext * client_context = itr->second; std::copy(buffer, buffer + size, std::back_inserter(client_context->m_packet)); size_t readSize = client_context->m_security.GetNextReadSize(&client_context->m_packet[0], client_context->m_packet.size()); if(readSize) { Network::PostRead(sid, readSize, true); return; } size_t readCount = client_context->m_security.GetReadProcessCount(&client_context->m_packet[0], client_context->m_packet.size()); if(readCount == 0) { Network::PostError(sid, -1, "GetReadProcessCount returned 0."); return; } std::pair<int, std::vector<char>> result = client_context->m_security.ExtractPacket(&client_context->m_packet[0], client_context->m_packet.size()); client_context->m_packet.clear(); if(result.first < 0) { Network::PostError(sid, -1, "ExtractPacket returned an error (%i).", result.first); return; } HandlePacket(client_context, result.second, (result.first == 1)); Network::PostRead(sid, client_context->m_security.GetNextReadSize(0, 0), true); } void OnSessionWrite(int sid, const char * const buffer, int size) { } void OnSessionError(int sid, int error, const char * msg) { printf("%s(%i, %i, %s)\n", __FUNCTION__, sid, error, strlen(msg) ? msg : "\"\""); std::map<int, ClientContext *>::iterator itr = m_current_clients.find(sid); if(itr != m_current_clients.end()) { delete itr->second; m_current_clients.erase(itr); } } void OnPoll() { } void OnShutdown() { } void OnLog(const char * const file, int line, const char * const func, const char * const text) { } private: void SendPacket(ClientContext * client_context, WORD opcode, StreamBuilder & data, bool encrypted) { StreamBuilder builder; builder.Write<WORD>(data.GetCount()); builder.Write<WORD>(opcode); builder.Write<WORD>(0); builder.WriteArray<char>(data.GetStream(), data.GetCount()); Send(client_context, builder.ToVector(), encrypted); } void SendPacket(ClientContext * client_context, WORD opcode, std::vector<char> & data, bool encrypted) { StreamBuilder builder; builder.Write<WORD>(data.size()); builder.Write<WORD>(opcode); builder.Write<WORD>(0); builder.WriteArray<char>(&data[0], data.size()); Send(client_context, builder.ToVector(), encrypted); } void Send(ClientContext * client_context, std::vector<char> output, bool encrypted) { printf("[S->C][%i][%.4X][%i bytes]%s\n", client_context->m_sid, *(unsigned short *)&output[2], *(unsigned short *)&output[0], encrypted ? "[Enc]" : ""); printf("%s\n\n", DumpToString(&output[0] + 6, output.size() - 6).c_str()); output = client_context->m_security.FormatS2CPacket(&output[0], output.size(), encrypted); PostWrite(client_context->m_sid, &output[0], output.size()); client_context->m_last_action = GetTickCount(); } bool HandlePacket(ClientContext * client_context, std::vector<char> & packet, bool encrypted) { StreamReader reader(&packet[0], packet.size()); WORD size = reader.Read<WORD>(); WORD opcode = reader.Read<WORD>(); WORD security = reader.Read<WORD>(); printf("[C->S][%i][%.4X][%i bytes]\n", client_context->m_sid, opcode, size); printf("%s\n\n", DumpToString(reader.GetCurStream(), reader.GetCurCount()).c_str()); if(opcode == 0x5000) { if(client_context->m_security.Handshake(&packet[0], packet.size()) == false) { Network::PostError(client_context->m_sid, 1, "Handshake returned false."); return false; } packet = client_context->m_security.BuildHandshakePacket(); Send(client_context, packet, false); return true; } if(opcode == 0x2001) { WORD len = reader.Read<WORD>(); std::string name; name.resize(len); reader.ReadArray(&name[0], len); BYTE type = reader.Read<BYTE>(); if(client_context->m_key == -1) { StreamBuilder builder; builder.Write<WORD>(14); builder.WriteArray<char>("GatewayServer", 14); builder.Write<BYTE>(0); SendPacket(client_context, 0x2001, builder, false); builder.Clear(); builder.Write<BYTE>(1); builder.Write<WORD>(1); builder.Write<WORD>(0x2005); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(0x00); // Data flag builder.Write<BYTE>(0x01); builder.Write<BYTE>(0x00); builder.Write<BYTE>(0x01); builder.Write<BYTE>(0x81); builder.Write<BYTE>(0x09); builder.Write<BYTE>(0x05); builder.Write<BYTE>(0x00); builder.Write<BYTE>(0x00); builder.Write<BYTE>(0x00); builder.Write<BYTE>(0x02); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(1); builder.Write<WORD>(1); builder.Write<WORD>(0x6005); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(0x00); // Data flag builder.Write<BYTE>(0x03); builder.Write<BYTE>(0x00); builder.Write<BYTE>(0x02); builder.Write<BYTE>(0x00); builder.Write<BYTE>(0x02); SendPacket(client_context, 0x600D, builder, false); } else if(client_context->m_key == -2) { StreamBuilder builder; builder.Write<WORD>(14); builder.WriteArray<char>("DownloadServer", 14); builder.Write<BYTE>(0); SendPacket(client_context, 0x2001, builder, false); } return true; } if(opcode == 0x6104) { StreamBuilder builder; builder.Write<BYTE>(1); builder.Write<WORD>(1); builder.Write<WORD>(0xA104); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(0x00); // Data flag builder.Write<BYTE>(0x00); SendPacket(client_context, 0x600D, builder, false); return true; } if(opcode == 0x6100) { BYTE locale = reader.Read<BYTE>(); WORD len = reader.Read<WORD>(); std::string name; name.resize(len); reader.ReadArray(&name[0], len); DWORD version = reader.Read<DWORD>(); if(version == current_version) { StreamBuilder builder; builder.Write<BYTE>(1); builder.Write<WORD>(1); builder.Write<WORD>(0xA100); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(0); builder.Write<BYTE>(1); SendPacket(client_context, 0x600D, builder, false); } // Not checking these so we can patch backwards and forwards as needed // for testing purposes. /* else if(version > current_version) // Too new { StreamBuilder builder; builder.Write<BYTE>(1); builder.Write<WORD>(1); builder.Write<WORD>(0xA100); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(0); builder.Write<BYTE>(2); builder.Write<BYTE>(1); SendPacket(client_context, 0x600D, builder, false); } else if(m_version_files.find(version + 1) == m_version_files.end() ) // Too old { StreamBuilder builder; builder.Write<BYTE>(1); builder.Write<WORD>(1); builder.Write<WORD>(0xA100); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(0); builder.Write<BYTE>(2); builder.Write<BYTE>(5); SendPacket(client_context, 0x600D, builder, false); } */ else { StreamBuilder builder; builder.Write<BYTE>(1); builder.Write<WORD>(1); builder.Write<WORD>(0xA100); SendPacket(client_context, 0x600D, builder, false); builder.Clear(); builder.Write<BYTE>(0); builder.Write<BYTE>(2); builder.Write<BYTE>(2); builder.Write<WORD>(9); builder.WriteArray<char>("127.0.0.1", 9); builder.Write<WORD>(16002); builder.Write<DWORD>(current_version); // TODO: A real DownloadServer would need to implement proper 0x600D building // but for this simple demo, I'll just set it up to assume only packet is needed. // // We must have keep incremental version folders with this design! // // DownloadServerDemo might cause corruption if anything isn't correct. This is only a simple // implementation to show the concepts, not to provide a full implementation. // // Temp condition so we can patch back and forth easily, this won't be in a real server! if(version > current_version) { version = current_version - 1; } for(size_t x = version + 1; x <= current_version; ++x) { std::map<int, std::vector<int>>::iterator itr = m_version_files.find(x); if(itr == m_version_files.end()) continue; for(size_t y = 0; y < itr->second.size(); ++y) { std::map<int, FileEntry>::iterator itr2 = m_server_files.find(itr->second[y]); if(itr2 == m_server_files.end()) continue; FileEntry & fileEntry = itr2->second; builder.Write<BYTE>(1); // new file builder.Write<DWORD>(fileEntry.m_id); builder.Write<WORD>(fileEntry.m_filename.size()); builder.WriteArray<char>(fileEntry.m_filename.c_str(), fileEntry.m_filename.size()); builder.Write<WORD>(fileEntry.m_filepath.size()); builder.WriteArray<char>(fileEntry.m_filepath.c_str(), fileEntry.m_filepath.size()); builder.Write<DWORD>(fileEntry.m_filesize); builder.Write<BYTE>(fileEntry.m_inpk2); } } builder.Write<BYTE>(0); SendPacket(client_context, 0x600D, builder, false); } } if(opcode == 0x6004) // Client requests a specific file { DWORD id = reader.Read<DWORD>(); DWORD unk2 = reader.Read<DWORD>(); // Might be the high word of the id, that's just not implemented std::map<int, FileEntry>::iterator itr2 = m_server_files.find(id); FileEntry & fileEntry = itr2->second; // Real server needs to handle invalid files! // The logic is simple, we send as many 0x1001 packets as needed containing the data. // Since the max packet size is 4096 total bytes, - 6 bytes header, that leaves us with // 4090 bytes of data. NOTE: My networking code handles packet queuing and dispatching // sends as needed. You cannot simply call WinSocks send(..) without checking to see // how many bytes were actually sent. StreamBuilder builder; size_t offset = 0; int input_data_size = fileEntry.m_filesize; unsigned char * input_data_ptr = (unsigned char *)&fileEntry.m_data[0]; while(input_data_size > 0) { if(input_data_size >= 4090) { builder.WriteArray<unsigned char>(input_data_ptr + offset, 4090); offset += 4090; input_data_size -= 4090; } else { builder.WriteArray<unsigned char>(input_data_ptr + offset, input_data_size); offset += input_data_size; input_data_size -= input_data_size; } SendPacket(client_context, 0x1001, builder, false); builder.Clear(); } builder.Clear(); // Queue up the packet that tells the client the file has been downloaded. builder.Write<BYTE>(0x01); SendPacket(client_context, 0xA004, builder, false); } return true; } public: DownloadServer() { m_file_index = 0; } ~DownloadServer() { } int Run(int argc, char * argv[]) { bool error = false; if(argc != 2) { printf("Usage:\n\tDownloadServerDemo <patch folder>\n"); return -1; } fs::path full_path(fs::initial_path<fs::path>()); if(PostAccept("127.0.0.1", 16000, -1) == false) error = true; if(PostAccept("127.0.0.1", 16002, -2) == false) error = true; if(!error) { // Somewhat messy logic, but basically we get a list of all version folders first. // Then, we collect all files for that version and store it into a second list. // This way, we can associate files with specific versions. Versions should // to be sequential though unless you implement otherwise. There are many ways // to accomplish this logic, I just choose one. // // To implement arbitrary version changing, you can specify an empty folder. // This way, a 'dummy.txt' file will be created (1 file is needed at minimal) // so the version can be changed through a patch. // std::list<std::string> folders; full_path = fs::system_complete(fs::path(argv[1])); if(!fs::exists(full_path)) { std::cout << "Error: Path not found: " << full_path.file_string() << std::endl; return -1; } get_top_level_folders(full_path, folders); std::list<std::string>::iterator itr = folders.begin(); while(itr != folders.end()) { std::list<std::string> files; std::string folder = (*itr); int version = atoi(folder.substr(1 + folder.find_last_of("\\//")).c_str()); recurse(fs::path(folder), files); std::list<std::string>::iterator itr2 = files.begin(); while(itr2 != files.end()) { std::string relpath = *itr2; if(!((relpath.find(".lzma") == relpath.size() - 5) || (relpath.find(".zlib") == relpath.size() - 5))) { ++itr2; continue; } relpath.erase(relpath.begin(), 1 + relpath.begin() + folder.size()); size_t ind = 0; std::string pth; ind = relpath.find_last_of("\\/"); if(ind != std::string::npos) { pth = relpath.substr(0, ind); } ind++; std::string nme; nme = relpath.substr(ind); nme = nme.substr(0, nme.size() - 5); FileEntry entry; entry.m_id = ++m_file_index; entry.m_inpk2 = 1; // hard coded for now, another system needs to be setup to distinguish this entry.m_filename = nme; entry.m_filepath = pth; FILE * inFile = 0; errno_t err = 0; if((err = fopen_s(&inFile, (*itr2).c_str(), "rb")) || inFile == NULL) { printf("Could not open the input file: [%s]\n", (*itr2).c_str()); return -1; } fseek(inFile, 0, SEEK_END); entry.m_filesize = ftell(inFile); fseek(inFile, 0, SEEK_SET); entry.m_data.resize(entry.m_filesize + 1); fread(&entry.m_data[0], 1, entry.m_filesize, inFile); fclose(inFile); m_server_files.insert(std::make_pair(entry.m_id, entry)); m_version_files[version].push_back(entry.m_id); ++itr2; } ++itr; } if(m_version_files.empty()) // We need at least one file to patch, so we will create an empty text file { // lzma (isro, rsro) BYTE data[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // zlib (ksro, csro) //BYTE data[] = { 0x00, 0x00, 0x00, 0x00, 0x78, 0x9C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01 }; FileEntry entry; entry.m_filename = "dummy.txt"; entry.m_filepath = ""; entry.m_filesize = sizeof(data); entry.m_id = 1; entry.m_inpk2 = 0; std::copy(data, data + sizeof(data), std::back_inserter(entry.m_data)); m_server_files.insert(std::make_pair(entry.m_id, entry)); m_version_files[current_version].push_back(entry.m_id); } while(!_kbhit()) { Poll(); Sleep(1); } } Shutdown(); if(error) return -1; return 0; } }; int main(int argc, char * argv[]) { return DownloadServer().Run(argc, argv); }

Now for some other comments. The project makes use of the Common code from my

Right now, there is not much documentation to the code since it's all fairly new and still being revised over time. However, I wanted to go ahead and share my latest findings through this project since this information has never been fully explored and released. As mentioned before, most people won't ever have a need for this information or project, but it's another piece of the Silkroad development puzzle.
The last section of this post I want to add in some information about how the DownloadServer works in conjunction with the GatewayServer. This should make following the code a bit easier in DownloadServerDemo and SilkroadPatchDownloader.
1. Client connects to GatewayServer, GatewayServer sends handshake, Client handshakes and responds.
2. Client sends its identify packet to the GatewayServer (0x2001, "SR_Client"), GatewayServer sends its identify packet to the Client (0x2001, "GatewayServer")
3a. Client sends its version packet to the GatewayServer (0x6100), GatewayServer checks version against current version and responds with 0xA100 (implemented through 0x600D, which is a 'big packet')
3b. If the version is too old or too new, 0xA100 contains a flag to tell the client this. If the version matches, then the flag tells the client it does. If there is a network problem, there is a flag to tell the client the servers are offline.
3c. If the version is outdated, but within the range of the patches stored on the server, then the 0xA100 packet contains DownloadServer information. The packet format is pretty simple for this mode:
4. The client connects to the DownloadServer just like it would to the WorldServer and repeats the 1st and 2nd steps.Quote:
[XX XX] - size
[00 A1] - opcode
[00 00] - security
[02] - error
[02] - updates available
[XX XX] - ip length
[XX ... XXX] - ip
[XX XX] - port
[XX XX XX XX] - current version
[XX] - new file flag? 1 = file, 0 = done
[XX XX XX XX] - file id, needed to send to the download server
[XX XX] - file name length
[XX ... XXX] - file name
[XX XX] - file path length
[XX ... XXX] - file path
[XX XX XX XX] - file size
[XX] - in PK2 flag
...
[00] - new file flag? 1 = file, 0 = done
5. Now comes the patching part. The client sends a file request through the 0x6004 packet, which simply is a DWORD of the file id followed by the DWORD0. I'd assume they were supporting 64bit ints, but Silkroad.exe only sends a DWORD.
6. The DownloadServer checks the id to obtain the file entry on the server and then sends the file in 4kb chunks, which is the max packet size, to the client through the 0x1001 packet. When all chunks have been sent, the 0xA004 packet is sent with a status code to signal success or failure (I only handle success, I've not looked into different errors yet).
7. The client buffers these 0x1001 packets until the 0xA004 packet is received. On success, it will decompress the file and then patch the PK2 using the GFXFileManager.dll as my original PK2Tools did. After the file has been patched, the client sends the next file to be downloaded through 0x6004 until all files have been patched.
8. After all files have been patched, Silkroad.exe runs replacer.exe with a special command line to patch media.pk2 with the latest version. Patches require at least one file at all times and the files have to be compressed using the format expected (lzma for rsro and isro, zlib for ksro and csro).
That wraps up how the DownloadServer works. Review the code to see a simple implementation of one. There's still a lot of work that would need to be done to make it ready for general use. Once you understand the concepts though, you can setup your own versioning system to make life easier on yourself.
Now, for emu projects, the tools do all the work of compressing and compressing, so only simple file loading code has to be implemented. It might be advisable to look into creating a tool that simply scans a folder for compressed files and generates a version 'package' list for the DownloadServer to use. Alternatively, you could write a tool that generates all of the packets that need to be sent and load them so you don't have to have your DownloadServer do any extra work. There's really a lot of different directions that you can go with it.
Phew! There's a lot of information to digest here as well as a lot of code to go through and understand. Don't expect to be able to understand everything right away. Make use of a Silkroad proxy if you need to trace packets or enable the debugging output in my tools through code to see everything step by step in real time.
Since there is so much here, I might have left out a few details or forgot to mention something, so if anything needs clarification, feel free to ask! I will be steadily working on more improved versions over the upcoming weeks while I work on other things.
Download: Attached (complete code + some binaries)
Virus Scan:


Enjoy!
