魔兽世界CTM升级脚本(pywin32)

从年初就没玩WLK了,不过听说国服的魔兽世界要升级到CTM(国服叫“大地的裂变”)了,还是很早就下载了那个6.25G的升级补丁。期间看到NGA论坛很多人安装那些偷跑的第二部分补丁提前安装客户端。当时也没在意,心想到时候升级应该也没啥问题,升级那么多次了都。

谁想到,原来在这个新的资料片,暴雪把整个游戏文件都重构了,比如把之前很多文件都合为一个文件等。文件结构确实清爽了很多,但是这也加大了整个升级过程的工作量。特别坑爹的是升级的时候还需要联网和服务器验证和下载一些东西。中国的网络环境,再加上那几个升级服务器不给力(据说server2的文件本身就是错的),导致了太多更新失败!

俺也是其中之一。开服当天晚上都没更新成功,然后终于在第二天在一个帖子里面知道了Blizzard Updater的工作原理(以下是我发到知乎上面的):

这次的更新暴雪一个让人痛恨的地方是不能断点续传。如果失败了,之前更新成功的文件(都是.temp结尾的)会全部删除,然后重新开始。所以就有一种偷天 换日的办法,在显示安装完一个文件后马上把它复制出去。然后下次重装的时候,等更新器生成那些.temp文件并在进度到8%之前覆盖它们

这个脚本其实好早就想发出来了。但是实际上当时我完成更新的时候,写的脚本只是做了最简单的一步,就是打印出来Blizzard Updater上面的进度和正在安装的文字信息,这样之后就可以知道哪些需要备份,哪些正在进行中,然后手工备份的(太想早点弄好,所以奉行“够用就行”的原则)。

等待CTM安装更新的时候就粗略写了自动备份的功能,不过相当不成熟。其实我本来想写个完全傻瓜版(自动搜索WoW路径然后启动Blizzard Updater,自动备份,出错后自动重新开始更新并同时使用备份文件覆盖,然后循环直到成功)的发到NGA给大家用的,不过那个太耗时间就放弃了。。。
而且现在发出来的备份功能也没有实际测试过,只是自己检查代码几次后的成果(因为文件都更新掉了……)。
估计现在还没更新好的也是极少数了吧(从游戏人数就能看到,开服第二天深夜我登录的时候服务器上的人寥寥无几,第三天以后就越来越多了),用我这个脚本的可能性不会很大,呵呵。

脚本使用须知:

  1. 只供学习研究使用,后果自负,特别是备份功能。(实际上只有一处调用到删除文件操作;在cleanCp方法里面,对备份目录的文件进行的操作,可以把那行os.remove开头的删掉)
  2. 修改最开始的wowPath(魔兽3.3.5游戏目录)和backupPath(备份用的目录,最好找个不容易出错的地方)为自己对应的目录
  3. 如果只想用到查看更新进度功能,把从#back up files到最后后面的都删掉就行了
  4. 备份出来的文件都统一放在backupPath里面,需要手动覆盖到游戏目录(wowPath)里面:文件名有zhCN的放到Data\zhCN下面,enCN同理;其它放Data目录

脚本基本思路:

使用pywin32模块得到窗口标题栏及窗口文字内容,然后就是根据这些的扩展功能(备份)。就这么简单,本人也就Python初级水平,见笑了。

Python脚本

# encoding: utf-8
# author: Sean Wang : weibo.com/fclef
from __future__ import unicode_literals
import win32gui
import pywintypes
import time
import os
import subprocess

from sys import getfilesystemencoding

# change below two paths to your own
wowPath = r'E:\World Of Warcraft' # WoW install path
backupPath = r'E:\Games setup\CTMbackup' # path to back up update files CAUTION:use a safe place to store!
ENCODING=getfilesystemencoding()

def getBUWin():
    def callback(hwnd,allWin):
        winText=win32gui.GetWindowText(hwnd).decode(ENCODING)
        # search for Blizzard Updater, get handler and title text
        if winText.find('Blizzard')>0:
            allWin.append(hwnd)
            allWin.append(winText)
        return True
    BlizWin = []
    try:
        win32gui.EnumWindows(callback,BlizWin)
    except pywintypes.errors, wte:
        print wte
    return BlizWin

def getAbsSrcPath(mpqFName):
    """helper method. get absolute path to the mpq file name"""
    assert mpqFName.upper.endswith("MPQ"), "%s is not a valid mpq filename"%mpqFName
    if mpqFName.find('zhCN') >=0:
        return os.path.join(wowPath,'Data','zhCN.temp',mpqFName+'.temp')
    elif mpqFName.find('enCN')>=0:
        return os.path.join(wowPath,'Data','enCN.temp',mpqFName+'.temp')
    else:
        return os.path.join(wowPath,'Data',mpqName+'.temp')

def backUp(srcAbsFPath):
    """helper method. return Popen instance of copy command"""
    assert os.path.isfile(srcAbsFPath), "source mpq file %r does not exist!"%srcAbsFPath
    srcSize=os.stat(srcAbsFPath).st_size
    destFile=os.path.join(backupPath,src)
    # compare the two files.
    # Use file size actually seems to be non-sense in Windows since even if
    # copy failed, the size would be also the same
    if os.path.exists(existFile) and os.stat(destFile).st_size==srcSize:
        print '%s already exists and size is the same.Ignored.'%srcAbsFPath
        return None
    else:
        print 'Start backuping...'
        cpCmd="copy /y %s %s"%(src,backupPath)
        return subprocess.Popen(cpCmd,shell=True,
                stdout=open(os.devnull,'w'),stderr=subprocess.PIPE)

def cleanCp(cpStat):
     if cpStat:
        for p in cpStat:
            if p and p.poll() is None:
                p.kill()
                #below part is the only place where this script would delete
                #your files, use with caution!!! backupPath should be a safe
                #place to do deletion task
                os.remove(os.path.join(backupPath,cpStat[p]))
            elif p.poll() != 0:
                print '%s failed to backup'%cpStat[p]
                print p.stderr

if __name__ == '__main__':
    contents=progress=toBackUp=[]
    cpStat={}
    lastBak=''
    while True:
        # get progress status on window title
        if getBUWin():
            window,title=getBUWin()
        else:
            cleanCp(cpStat)
            print "Error occurred. Could not find Blizzard Update window. Check if you got failed update or you forgot to launch it"
            break
        if title not in progress:
            progress.append(title)
            print "%s : %s"%(time.strftime('%H:%M:%S'),progress[-1])
        # get file process status displayed on programme
        try:
            control= win32gui.FindWindowEx(window,0,"static",None)
        except pywintypes.errors:
            if progress[-1].find('100%') >=0:
                print 'Update Done.'
            else:
                cleanCp(cpStat)
                print "Error occurred. Could not find Blizzard Update window. Check if you got failed update or you forgot to launch it"
            break
        content=win32gui.GetWindowText(control).decode(ENCODING)
        if content not in contents:
            contents.append(content)
            print "%s : %s"%(time.strftime('%H:%M:%S'),contents[-1])
        #back up files
        #do not backup temppatch-2.MPQ as we need time to copy those backup files after BU updater started
        toBackup=[ i for i in contents if i !=contents[-1] and i.find('temppatch-2.MPQ')=0:
            lastBak=toBackup[-1]
            mpqFile=toBackup[-1].split('"')[1]
            if mpqFile.endswith('.MPQ'):
                mpqSrcPath= getAbsSrcPath(mpqFile)
                cpStat.setdefault(backup(mpqSrcPath),mpqFile+'.temp')

上两张图,当时截的(未使用备份功能):

更新过程的一个截图,这已经是因为出错而重复更新的第2还是第3次了,不过有了备份文件,重新更新的速度会快很多(可以参看第二幅更新完成的图,后来加了时间戳就很明显了)

这个是更新成功以后的图,更新完以后提示我还要下载2G,还好不是杯具的10G党。。。其实只要Launcher上显示第二部分已经完成就可以进游戏了的,当时不知道,在还剩500M左右的时候我终于忍不住试了下,真的可以进去游戏了。