魔兽世界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左右的时候我终于忍不住试了下,真的可以进去游戏了。

用Ruby替换魔兽世界toc文件版本号

玩魔兽世界的同学都知道,当WoW升级后,插件也需要升级。但是有些插件其实没有影响,只是需要把toc文件里面的版本号改成新的就好。这几天刚好国服从3.2.2升级到了3.3.5,这样toc里面的版本号就需要从30200改成30300 (虽然有些改了也用不了)

然后我就想到用ruby实现这个应该不难,早上起来就开始写了。其实很简单,只是自己对ruby的应用还不纯熟。。。现在终于完工了

代码如下:

# toc_ver.rb
# change version number of toc to what you wanted
if ARGV.length != 2 || ARGV.last !~ /[0-9]{5}/ || !File.directory?(ARGV.first)
 puts "Error: two arguments should be used. If your path contains spaces, using quotes" if ARGV.length !=2
 puts "Error: specified directory not found" if File.directory?(ARGV.first)
 puts "Error: version number should be 5 digits" if ARGV.last !~ /[0-9]{5}/
 puts usage=<<USAGE
Usage: toc_ver.rb path_of_Addons version_number
Example: toc_ver.rb "E:\\World of Warcaft\\Interface\\Addons" 30300
USAGE
exit
end

Dir.chdir(ARGV.first)
tocfiles= File.join("**","*.toc")
Dir.glob(tocfiles) do |filename|
#save the file lines in an array; substitution; overwrite the original file
file_array=File.readlines(filename).each {|line| line.gsub!(/## Interface: [0-9]{5}/, "## Interface: #{ARGV.last}") }
 File.open(filename,'w') do |file|
 file_array.each {|line| file.puts(line)}
 end
end

把上面代码保存为toc_ver.rb使用方法: toc_ver.rb [Addons的路径] [要修改的版本号]。
如 toc_ver.rb “E:\World of Warcaft\Interface\Addons” 30300
注意,如果路径有空格的话需要加引号。

浪费的时间主要是想:打开一个文件,然后读入每行。找到要修改的行,替换。
这种想法是不行的,不能对一个文件同时读+修改。所以我还是先把每行读到一股数组,修改完之后再把数组写入。可以参见Stackedoverflow的讨论。那个正确答案是错的,File.read返回的是File对象而不是String

WoW

Today is a day worth remembering in my WoW career.
My first lvl 60 character comes true, not as happy as I wished…
It’s a pity that forgetting to get a snapshot for the ‘ding’ moment…
 
After 11 hours , The Darnassus reputation became to exalted!!!  Got 3 snapshots but either has the green light.
All this is for the Reins of the Swift Frostsaber
 
But now I’m the really poor man…
In debt now… how much? have forgetton, all in a word: so much 🙂