I provide you here an ingame patcher for the Metin2 Client, written in C++. It is not really perfect since it might bug around when you open multiple clients, but it is perfect if you just want to create a server for yourself and your friends (like I did xD) and you don't want to send those files over every fucking time something changes.
It is fast and you only need a simple web server (like, the server where your metin2 server is on) to add it into your client! But it is NOT robust as I already said. Don't try use it on a real server for real customers / players - they will annoy you that something doesn't work because they have multiple windows open!
It is not the best code quality but I guess it's readable and I won't waste time on that code that probably just goes into the trash bin once we stop play on this server x)
The C++ Part:
Add the files M2WebSocket.cpp, M2WebSocket.h, M2WebSocketModule.cpp into your UserInterface project and call the module initialization function by adding
Code:
initM2WebSocket();
Code:
bool RunMainScript(CPythonLauncher& pyLauncher, const char* lpCmdLine)
Code:
void initM2WebSocket();
That was all for the C++ part already, crazy easy isn't it?!
The Python Part:
Open the intrologin.py (in root) and add the following imports to the top of the file:
Code:
import hashlib import m2socket from subprocess import Popen, PIPE
Code:
class Patcher: UPDATE_FILE_NAME = "updatePatchFiles.bat" CREATE_NEW_PROCESS_GROUP = 0x00000200 DETACHED_PROCESS = 0x00000008 STATE_RECV_FILE_LIST = 0 STATE_RECV_FILES = 1 STATE_RESTART = 2 def __init__(self, infoText, progressBar, onDoneFunc): self._state = self.STATE_RECV_FILE_LIST self._socket = m2socket.Create(constInfo.PATCH_SERVER_ADDR, constInfo.PATCH_GET_FILE_LIST) self._infoText = infoText infoText.SetText(localeInfo.LOGIN_PATCH_LOAD_FILE_LIST) self._progressBar = progressBar self._onDoneFunc = onDoneFunc self._downloadIndex = 0 self._fileList = [] self._entireRequiredSize = 0 self._entireDownloadSize = 0 if os.path.exists(self.UPDATE_FILE_NAME): os.remove(self.UPDATE_FILE_NAME) def __del__(self): self.__ClearSocket() def __ClearSocket(self): if self._socket != 0: m2socket.Destroy(self._socket) self._socket = 0 def __UpdateProgressBar(self, currentSize): entireDownloadSize = self._entireDownloadSize + currentSize self._progressBar.SetPercentage(entireDownloadSize, self._entireRequiredSize) def __StartDownload(self, index): self._downloadIndex = index fileName, _, _ = self._fileList[index] self._infoText.SetText(str(localeInfo.LOGIN_PATCH_DOWNLOAD_FILE % (fileName, 0.0, 0.0))) self.__UpdateProgressBar(0) self.__ClearSocket() self._socket = m2socket.Create(constInfo.PATCH_SERVER_ADDR, constInfo.PATCH_ROOT + fileName) def __StateRecvFileList(self): if not m2socket.IsFinished(self._socket): return if m2socket.IsError(self._socket): self.__ClearSocket() self._onDoneFunc() return fileList = m2socket.GetData(self._socket).split("<br>") self.__ClearSocket() for i in range(len(fileList) - 1, -1, -1): if fileList[i]: fileName, fileMd5, fileSize = fileList[i].split("|") if os.path.exists(fileName): with open(fileName, "rb") as f: localFileMd5 = hashlib.md5(f.read()).hexdigest() if localFileMd5 == fileMd5: continue self._fileList.append((fileName, fileMd5, int(fileSize))) self._entireRequiredSize += int(fileSize) if len(self._fileList) == 0: self._onDoneFunc() return # extract update file already because if root is overwritten, this is no longer accessible updateFileData = pack_open(self.UPDATE_FILE_NAME).read() with open(self.UPDATE_FILE_NAME, "wb") as f: f.write(updateFileData) self._state = self.STATE_RECV_FILES self.__StartDownload(0) def __StateRecvFiles(self): fileName, fileMd5, fileSize = self._fileList[self._downloadIndex] if not m2socket.IsFinished(self._socket): if fileSize > 0: downloadSize = m2socket.GetCurrentSize(self._socket) downloadPercent = downloadSize * 100 / fileSize downloadSpeed = m2socket.GetCurrentSpeed(self._socket) / 1024.0 / 1024.0 self._infoText.SetText(str(localeInfo.LOGIN_PATCH_DOWNLOAD_FILE % (fileName, downloadPercent, downloadSpeed))) self.__UpdateProgressBar(downloadSize) return self._entireDownloadSize += fileSize self.__UpdateProgressBar(0) if fileName.endswith(".exe"): fileName += ".temppatch" m2socket.SaveToFile(self._socket, fileName) self.__ClearSocket() index = self._downloadIndex + 1 if index >= len(self._fileList): self._state = self.STATE_RESTART self.__RestartApplication() return self.__StartDownload(index) def __RestartApplication(self): Popen(["updatePatchFiles.bat"], stdin=PIPE, stdout=PIPE, stderr=PIPE, creationflags=self.DETACHED_PROCESS | self.CREATE_NEW_PROCESS_GROUP) app.Exit() def Update(self): if self._state == self.STATE_RECV_FILE_LIST: self.__StateRecvFileList() elif self._state == self.STATE_RECV_FILES: self.__StateRecvFiles()
Code:
def __init__(self, stream):
Code:
self._patcher = None
Code:
def Close(self):
Code:
self.patchBoard = None self.patchTitle = None self.patchBar = None self._patcher = None
Code:
self.patchBoard = GetObject("PatchBoard") self.patchTitle = GetObject("PatchTitle") self.patchBar = GetObject("PatchBar")
Code:
if self._patcher is not None: self._patcher.Update()
Code:
def __OpenPatchBoard(self): if not constInfo.APP_START: self.__OpenServerBoard() return constInfo.APP_START = False # RUNUP_MATRIX_AUTH if IsRunupMatrixAuth(): self.matrixQuizBoard.Hide() # RUNUP_MATRIX_AUTH_END # NEWCIBN_PASSPOD_AUTH if IsNEWCIBNPassPodAuth(): self.passpodBoard.Hide() # NEWCIBN_PASSPOD_AUTH_END self.patchBoard.Show() self.serverBoard.Hide() self.loginBoard.Hide() self.connectBoard.Hide() if self.virtualKeyboard: self.virtualKeyboard.Hide() self.patchBar.SetPercentage(0, 100) self._patcher = Patcher(self.patchTitle, self.patchBar, ui.__mem_func__(self.__OpenServerBoard))
Code:
self._patcher = None
Code:
self.__OpenServerBoard()
Code:
self.__OpenPatchBoard()
Open the loginwindow.py you are using (either in locale_de or root normally) and add the PatchBoard as children of the LoginWindow:
Code:
## PatchBoard { "name" : "PatchBoard", "type" : "thinboard", "x" : 0, "y" : 0, "width" : 350, "height" : 62, "horizontal_align" : "center", "vertical_align" : "center", "children" : ( { "name" : "PatchTitle", "type" : "text", "x" : 0, "y" : 12, "horizontal_align" : "center", "text_horizontal_align" : "center", }, { "name" : "PatchBarBg", "type" : "image", "x" : 0, "y" : 30, "horizontal_align" : "center", "vertical_align" : "bottom", "image" : uiScriptLocale.LOCALE_UISCRIPT_PATH + "loading/gauge_empty.sub", "children" : ( { "name" : "PatchBar", "type" : "expanded_image", "x" : 15, "y" : 5, "image" : uiScriptLocale.LOCALE_UISCRIPT_PATH + "loading/gauge_full.sub", }, ), }, ), },
Code:
# indicates that the app is started and login window is shown for the first time APP_START = True PATCH_SERVER_ADDR = "my-domain-name.de" PATCH_ROOT = "m2patcher/" PATCH_GET_FILE_LIST = PATCH_ROOT + "get_file_list.php"
Code:
LOGIN_PATCH_LOAD_FILE_LIST Suche nach aktualisierten Dateien... LOGIN_PATCH_DOWNLOAD_FILE Aktualisiere %s... (%.0f%%, %.02f MB/s)
Alright, you are done (hopefully I didn't forget anything lel).
The web server side:
On the webserver you just need to create a folder called m2patcher on your domain and put the file get_file_list.php there. Then you can upload any files into this directory and they will get patched if they are different on the client. Of course, you need to create the same structure for any file (e.g. if you want to patch some files in the pack folder, create the pack folder and put the files in there).
Yeee you are done! You have now a simple and fast ingame patcher for you and your friends x3