Beispiel #1
0
class LeeBaseTranslator:
    def __init__(self):
        self.leeCommon = LeeCommon()
        self.leeFileIO = None
        self.translateDefaultDBPath = None
        self.translateMap = {}
        self.reSrcPathPattern = None
        self.reDstPathPattern = None

    def clear(self):
        self.translateMap.clear()

    def load(self, translateDBPath = None):
        self.clear()
        if translateDBPath is None:
            translateDBPath = self.translateDefaultDBPath
        translatePath = '%s/%s' % (self.leeCommon.utility(withmark=False), translateDBPath)
        if not self.leeCommon.isFileExists(translatePath):
            return False
        try:
            self.translateMap = json.load(open(translatePath, 'r', encoding = 'utf-8'))
            return True
        except FileNotFoundError as _err:
            print('很抱歉, 无法打开翻译数据库文件: %s' % translatePath)
            raise

    def save(self, translateDBPath = None):
        if translateDBPath is None:
            translateDBPath = self.translateDefaultDBPath
        savePath = self.leeCommon.utility(translateDBPath)
        json.dump(
            self.translateMap, open(savePath, 'w', encoding = 'utf-8', newline = '\n'),
            indent = 4, ensure_ascii = False
        )
        return True

    def doTranslate(self, specifiedClientVer = None):
        leeClientDir = self.leeCommon.client()
        patchesDir = self.leeCommon.patches()

        if self.reSrcPathPattern is None:
            return False
        if self.reDstPathPattern is None:
            return False

        sourceFilepathList = []
        for dirpath, _dirnames, filenames in os.walk(patchesDir):
            for filename in filenames:
                fullpath = os.path.normpath('%s/%s' % (dirpath, filename))
                if not re.match(self.reSrcPathPattern, fullpath, re.I):
                    continue
                sourceFilepathList.append(fullpath)

        self.load()

        for sourceFilepath in sourceFilepathList:
            if (specifiedClientVer is not None) and (specifiedClientVer not in sourceFilepath):
                continue
            print('正在汉化, 请稍候: %s' % os.path.relpath(sourceFilepath, leeClientDir))
            match = re.search(
                self.reDstPathPattern, sourceFilepath, re.MULTILINE | re.IGNORECASE | re.DOTALL
            )
            if match is None:
                self.leeCommon.exitWithMessage('无法确定翻译后的文件的存放位置, 程序终止')
            destinationPath = '%s/Translated/%s' % (match.group(1), match.group(2))
            self.translate(sourceFilepath, destinationPath)
            print('汉化完毕, 保存到: %s\r\n' % os.path.relpath(destinationPath, leeClientDir))

        return True

    def translate(self, srcFilepath, dstFilepath):
        pass
Beispiel #2
0
class LeeVerifier:
    '''
    此操作类用于验证客户端的文件是否完整
    '''
    def __init__(self):
        self.leeCommon = LeeCommon()
        self.leeParser = LeeStructParser()
        self.textureDirs = ['data/texture']
        self.modelDirs = ['data/model']
        self.spriteDirs = ['data/sprite']

        self.reportInfo = []
        self.reportStartTime = 0  # 用于记录当前检测项目的启动时间
        self.reportFileCount = 0  # 记录本次检测项目中, 丢失了资源的文件数量
        self.reportMissCount = 0  # 记录本次检测项目中, 累计丢失的资源文件数量

    def __verifyGnd(self, gndfilepath, priorityDataDir=None):
        result, texturePathList = self.leeParser.parseGndFile(gndfilepath)
        if not result:
            return None, None

        missTexturePathList = []
        existsTexturePathList = []
        leeClientDir = self.leeCommon.client(withmark=False)

        for texturePath in texturePathList:
            if priorityDataDir:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/texture/%s' %
                    (leeClientDir, priorityDataDir, texturePath))
                # print(fullpath)
                if self.leeCommon.isFileExists(fullpath):
                    existsTexturePathList.append(fullpath)
                    continue
            for textureDir in self.textureDirs:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/%s' % (leeClientDir, textureDir, texturePath))
                if self.leeCommon.isFileExists(fullpath):
                    existsTexturePathList.append(fullpath)
                    break
            else:
                # print(texturePath)
                missTexturePathList.append(fullpath)

        return existsTexturePathList, missTexturePathList

    def __verifyRsw(self, rswfilepath, priorityDataDir=None):
        result, modelPathList = self.leeParser.parseRswFile(rswfilepath)
        if not result:
            return None, None

        missModelPathList = []
        existsModelPathList = []
        leeClientDir = self.leeCommon.client(withmark=False)

        for modelPath in modelPathList:
            if priorityDataDir:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/model/%s' %
                    (leeClientDir, priorityDataDir, modelPath))
                # print(fullpath)
                if self.leeCommon.isFileExists(fullpath):
                    existsModelPathList.append(fullpath)
                    continue
            for modelDir in self.modelDirs:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/%s' % (leeClientDir, modelDir, modelPath))
                if self.leeCommon.isFileExists(fullpath):
                    # print('existsModelPathList: %s' % modelPath)
                    existsModelPathList.append(fullpath)
                    break
            else:
                # print('missModelPathList: %s' % modelPath)
                missModelPathList.append(fullpath)

        return existsModelPathList, missModelPathList

    def __verifyRsm(self, rsmfilepath, priorityDataDir=None):
        result, texturePathList = self.leeParser.parseRsmFile(rsmfilepath)
        if not result:
            return None, None

        missTexturePathList = []
        existsTexturePathList = []
        leeClientDir = self.leeCommon.client(withmark=False)

        for texturePath in texturePathList:
            if priorityDataDir:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/texture/%s' %
                    (leeClientDir, priorityDataDir, texturePath))
                # print(fullpath)
                if self.leeCommon.isFileExists(fullpath):
                    existsTexturePathList.append(fullpath)
                    continue
            for textureDir in self.textureDirs:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/%s' % (leeClientDir, textureDir, texturePath))
                if self.leeCommon.isFileExists(fullpath):
                    existsTexturePathList.append(fullpath)
                    break
            else:
                # print(texturePath)
                missTexturePathList.append(fullpath)

        return existsTexturePathList, missTexturePathList

    def __verifyStr(self, strfilepath, priorityDataDir=None):
        result, texturePathList = self.leeParser.parseStrFile(strfilepath)
        if not result:
            return None, None
        texturePathList = list(
            set(texturePathList))  # 文件名消重(str解析出来重复的图档文件名太多)

        missTexturePathList = []
        existsTexturePathList = []

        leeClientDir = self.leeCommon.client(withmark=False)
        leeClientCommonDataDir = '%s/data' % leeClientDir
        isPatchStrFile = leeClientCommonDataDir.lower(
        ) not in strfilepath.lower()

        dataPostion = strfilepath.lower().rfind('data/')
        strfileDirBaseonData = os.path.dirname(strfilepath[dataPostion:])

        if priorityDataDir is not None and priorityDataDir.lower().endswith(
                '/data'):
            priorityDataDir = priorityDataDir[:-len('/data')]

        for texturePath in texturePathList:
            if isPatchStrFile and priorityDataDir is not None:
                # leeClientDir + priorityDataDir + strfileDirBaseonData + 文件名
                fullpath = self.leeCommon.normpath(
                    '%s/%s/%s/%s' % (leeClientDir, priorityDataDir,
                                     strfileDirBaseonData, texturePath))
                if self.leeCommon.isFileExists(fullpath):
                    existsTexturePathList.append(fullpath)
                    continue

                # leeClientDir + priorityDataDir + self.textureDirs + '/effect' + 文件名
                isFound = False
                for textureDir in self.textureDirs:
                    fullpath = self.leeCommon.normpath(
                        '%s/%s/%s/effect/%s' % (leeClientDir, priorityDataDir,
                                                textureDir, texturePath))
                    if self.leeCommon.isFileExists(fullpath):
                        existsTexturePathList.append(fullpath)
                        isFound = True
                        break
                if isFound:
                    continue

            # leeClientDir + strfileDirBaseonData + 文件名
            fullpath = self.leeCommon.normpath(
                '%s/%s/%s' % (leeClientDir, strfileDirBaseonData, texturePath))
            if self.leeCommon.isFileExists(fullpath):
                existsTexturePathList.append(fullpath)
                continue

            # leeClientDir + self.textureDirs + '/effect'
            isFound = False
            for textureDir in self.textureDirs:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/effect/%s' %
                    (leeClientDir, textureDir, texturePath))
                if self.leeCommon.isFileExists(fullpath):
                    existsTexturePathList.append(fullpath)
                    isFound = True
                    break
            if not isFound:
                missTexturePathList.append(fullpath)

        return existsTexturePathList, missTexturePathList

    def __verifyIteminfo(self, iteminfofilepath, priorityDataDir=None):
        result, texturePathList, spritePathList = self.leeParser.parseIteminfo(
            iteminfofilepath)
        if not result:
            return None, None

        leeClientDir = self.leeCommon.client(withmark=False)

        missTexturePathList = []
        for texturePath in texturePathList:
            if priorityDataDir:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/texture/%s' %
                    (leeClientDir, priorityDataDir, texturePath))
                # print(fullpath)
                if self.leeCommon.isFileExists(fullpath):
                    continue
            for textureDir in self.textureDirs:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/%s' % (leeClientDir, textureDir, texturePath))
                if self.leeCommon.isFileExists(fullpath):
                    break
            else:
                # print('missTexturePathList: %s' % texturePath)
                missTexturePathList.append(fullpath)

        missSpritePathList = []
        for spritePath in spritePathList:
            if priorityDataDir:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/sprite/%s' %
                    (leeClientDir, priorityDataDir, spritePath))
                # print(fullpath)
                if self.leeCommon.isFileExists(fullpath):
                    continue
            for spriteDir in self.spriteDirs:
                fullpath = self.leeCommon.normpath(
                    '%s/%s/%s' % (leeClientDir, spriteDir, spritePath))
                if self.leeCommon.isFileExists(fullpath):
                    break
            else:
                # print('missSpritePathList: %s' % spritePath)
                missSpritePathList.append(fullpath)

        return missTexturePathList, missSpritePathList

    def __resetReport(self):
        self.reportInfo.clear()

    def __appendReportMessage(self, mesType, message):
        if mesType.lower() == 'header':
            self.reportInfo.append(
                '=============================================')
            self.reportInfo.append(message)
            self.reportInfo.append(
                '=============================================')
            # 启动一个计时器和一个 __appendReportData 计数器
            self.reportFileCount = self.reportMissCount = 0
            self.reportStartTime = timeit.default_timer()
        elif mesType.lower() == 'footer':
            # 总结耗时以及写入一些统计信息
            spendTime = timeit.default_timer() - self.reportStartTime
            resourceInfo = '非常好, 此项目无任何文件缺失!' if self.reportFileCount == 0 else '有 %d 个文件共计缺失 %d 个资源' % (
                self.reportFileCount, self.reportMissCount)
            self.reportInfo.append('%s / 耗时: %.2f 秒' %
                                   (resourceInfo, spendTime))
            self.reportInfo.append(
                '=============================================')
            self.reportInfo.append('')
            self.reportInfo.append('')

    def __appendReportData(self, sourceFile, missFilesList):
        leeClientDir = self.leeCommon.client()
        relSourceFile = os.path.relpath(sourceFile, leeClientDir)

        missFileCount = 0
        for fileslist in missFilesList:
            if not fileslist or not fileslist['files']:
                continue
            missFileCount = missFileCount + len(fileslist['files'])

        if not missFileCount:
            return

        self.reportInfo.append('>>> %s (缺失 %d 个文件)' %
                               (relSourceFile, missFileCount))

        for fileslist in missFilesList:
            if not fileslist:
                continue
            for missFile in fileslist['files']:
                self.reportInfo.append(
                    '    缺失%s: %s' % (fileslist['name'],
                                      os.path.relpath(missFile, leeClientDir)))

        self.reportInfo.append('')
        self.reportFileCount = self.reportFileCount + 1
        self.reportMissCount = self.reportMissCount + missFileCount

    def __saveReport(self):
        reportTime = time.strftime("%Y%m%d_%H%M%S", time.localtime())
        savePath = '%s/Reports/VerifyRpt_%s.txt' % (self.leeCommon.utility(
            withmark=False), reportTime)
        savePath = self.leeCommon.normpath(savePath)
        os.makedirs(os.path.dirname(savePath), exist_ok=True)

        rptfile = open(savePath, 'w+', encoding='utf-8', newline='')
        rptfile.write('\r\n'.join(self.reportInfo))

        print('校验结果已保存到 : %s' %
              os.path.relpath(savePath, self.leeCommon.client()))

    def __getFilesInfo(self,
                       glob_or_re,
                       reWalkDir,
                       pattern,
                       baseDir_or_reGroupID,
                       baseDir_append=None):
        filesinfo = []

        if glob_or_re == 'glob':
            for filepath in glob.glob(pattern):
                datadir = baseDir_or_reGroupID
                if baseDir_append:
                    datadir = datadir + baseDir_append
                filesinfo.append({'datadir': datadir, 'filepath': filepath})
        elif glob_or_re == 're':
            pattern = self.leeCommon.normPattern(pattern)
            for dirpath, _dirnames, filenames in os.walk(reWalkDir):
                for filename in filenames:
                    fullpath = os.path.normpath('%s/%s' % (dirpath, filename))
                    matches = re.match(pattern, fullpath, re.I)
                    if not matches:
                        continue

                    datadir = None
                    if baseDir_or_reGroupID is not None:
                        datadir = matches.group(baseDir_or_reGroupID)
                    if datadir and baseDir_append:
                        datadir = datadir + baseDir_append

                    filesinfo.append({
                        'datadir': datadir,
                        'filepath': fullpath
                    })
        else:
            self.leeCommon.exitWithMessage('指定的 glob_or_re 的值无效, 程序终止')

        return filesinfo

    def __subVerifier(self,
                      filesinfo,
                      parsefunc,
                      subject,
                      returnPathListIndex=None,
                      reportMissInfo=None):

        if filesinfo is None:
            return None

        self.__appendReportMessage('header',
                                   '%s - 共 %d 个' % (subject, len(filesinfo)))
        print('正在%s - 共 %d 个' % (subject, len(filesinfo)))

        if filesinfo and not isinstance(filesinfo[0], dict):
            restructFilesinfo = []
            for filepath in filesinfo:
                restructFilesinfo.append({
                    'filepath': filepath,
                    'datadir': None
                })
            filesinfo = restructFilesinfo

        for fileinfo in filesinfo:
            filepath = fileinfo['filepath']
            datadir = fileinfo['datadir']
            parsefuncResult = parsefunc(filepath, datadir)

            if not reportMissInfo:
                continue

            needReportFilelist = []
            for missinfo in reportMissInfo:
                needReportFilelist.append({
                    'name':
                    missinfo['name'],
                    'files':
                    parsefuncResult[missinfo['resultIndex']]
                })

            self.__appendReportData(filepath, needReportFilelist)
        self.__appendReportMessage('footer', '')

        if returnPathListIndex is None:
            return None
        return parsefuncResult[returnPathListIndex]

    def __globalResourceVerifier(self):
        leeClientDir = self.leeCommon.client(withmark=False)

        # 校验公用目录中地图文件所需的图档文件
        # =====================================================================

        # 校验地图的 gnd 文件(纹理层)

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='glob',
            reWalkDir=None,
            pattern='%s/data/*.gnd' % leeClientDir,
            baseDir_or_reGroupID=None,
            baseDir_append=None),
                           parsefunc=self.__verifyGnd,
                           subject='校验通用资源目录中的 gnd 文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': '地表贴图',
                               'resultIndex': 1
                           }])

        # 校验地图的 rsw 文件(模型层)

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='glob',
            reWalkDir=None,
            pattern='%s/data/*.rsw' % leeClientDir,
            baseDir_or_reGroupID=None,
            baseDir_append=None),
                           parsefunc=self.__verifyRsw,
                           subject='校验通用资源目录中的 rsw 文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': 'RSM模型文件',
                               'resultIndex': 1
                           }])

        # 校验地图中 rsm 模型文件的贴图

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir='%s/data' % leeClientDir,
            pattern=r'^.*?/data/.*?\.(rsm)',
            baseDir_or_reGroupID=None,
            baseDir_append=None),
                           parsefunc=self.__verifyRsm,
                           subject='校验通用资源目录中的 rsm 模型的贴图文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': 'RSM模型文件的贴图文件',
                               'resultIndex': 1
                           }])

        # 校验公用目录中动画效果索引文件 str 中所需的贴图
        # =====================================================================

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir='%s/data' % leeClientDir,
            pattern=r'^.*?/data/.*?\.(str)',
            baseDir_or_reGroupID=None,
            baseDir_append=None),
                           parsefunc=self.__verifyStr,
                           subject='校验通用资源目录中 str 文档所需的贴图文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': '动画索引贴图文件',
                               'resultIndex': 1
                           }])

        # 校验各个补丁目录中 Iteminfo 文件中所需的贴图
        # =====================================================================

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir='%s/System' % leeClientDir,
            pattern=r'^.*?/iteminfo.*?\.(lua|lub)',
            baseDir_or_reGroupID=None,
            baseDir_append=None),
                           parsefunc=self.__verifyIteminfo,
                           subject='校验通用资源目录中的 iteminfo 道具描述文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': '道具图片',
                               'resultIndex': 0
                           }, {
                               'name': '掉落和拖动时的图档',
                               'resultIndex': 1
                           }])

    def __patchesResourceVerifier(self):
        patchesDir = self.leeCommon.patches()

        # 校验各个补丁目录中地图文件所需的图档文件
        # =====================================================================

        # 校验地图的 gnd 文件(纹理层)

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir=patchesDir,
            pattern=
            r'^.*?/(Utility/Patches/.*?/Resource/Original/data)/.*?\.(gnd)',
            baseDir_or_reGroupID=1,
            baseDir_append=None),
                           parsefunc=self.__verifyGnd,
                           subject='校验各补丁目录中的 gnd 文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': '地表贴图',
                               'resultIndex': 1
                           }])

        # 校验地图的 rsw 文件(模型层)

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir=patchesDir,
            pattern=
            r'^.*?/(Utility/Patches/.*?/Resource/Original/data)/.*?\.(rsw)',
            baseDir_or_reGroupID=1,
            baseDir_append=None),
                           parsefunc=self.__verifyRsw,
                           subject='校验各补丁目录中的 rsw 文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': 'RSM模型文件',
                               'resultIndex': 1
                           }])

        # 校验地图中 rsm 模型文件的贴图

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir=patchesDir,
            pattern=
            r'^.*?/(Utility/Patches/.*?/Resource/Original/data)/.*?\.(rsm)',
            baseDir_or_reGroupID=1,
            baseDir_append=None),
                           parsefunc=self.__verifyRsm,
                           subject='校验各补丁目录中的 rsm 模型的贴图文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': 'RSM模型文件的贴图文件',
                               'resultIndex': 1
                           }])

        # 校验各个补丁目录中动画效果索引文件 str 中所需的贴图
        # =====================================================================

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir=patchesDir,
            pattern=
            r'^.*?/(Utility/Patches/.*?/Resource/Original/data)/.*?\.(str)',
            baseDir_or_reGroupID=1,
            baseDir_append=None),
                           parsefunc=self.__verifyStr,
                           subject='校验各补丁目录中 str 文档所需的贴图文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': '动画索引贴图文件',
                               'resultIndex': 1
                           }])

        # 校验各个补丁目录中 Iteminfo 文件中所需的贴图
        # =====================================================================

        self.__subVerifier(filesinfo=self.__getFilesInfo(
            glob_or_re='re',
            reWalkDir=patchesDir,
            pattern=r'^.*?/(Utility/Patches/.*?/Resource/Original)/System/' +
            r'iteminfo.*?\.(lua|lub)',
            baseDir_or_reGroupID=1,
            baseDir_append='/data'),
                           parsefunc=self.__verifyIteminfo,
                           subject='校验各补丁目录中的 iteminfo 道具描述文件',
                           returnPathListIndex=None,
                           reportMissInfo=[{
                               'name': '道具图片',
                               'resultIndex': 0
                           }, {
                               'name': '掉落和拖动时的图档',
                               'resultIndex': 1
                           }])

    def runVerifier(self):
        self.__resetReport()
        self.__globalResourceVerifier()
        self.__patchesResourceVerifier()
        self.__saveReport()
Beispiel #3
0
class LeeMenu:
    def __init__(self):
        self.patchManager = LeePatchManager()
        self.buttonTranslator = LeeButtonTranslator()
        self.leeVerifier = LeeVerifier()
        self.leeCommon = LeeCommon()

    def menufuncResetWorkshop(self):
        '''
        重置 LeeClient 客户端环境
        为接下来切换其他版本的客户端做好准备
        '''
        try:
            print('正在重置按钮汉化文件 ...')
            self.buttonTranslator.doRevert('AllVersions')

            print('正在重置其他客户端资源 ...')
            self.patchManager.doRevertPatch()

            leeClientDir = self.leeCommon.client(withmark=False)
            if self.leeCommon.isFileExists('%s/data.grf' % leeClientDir):
                os.remove('%s/data.grf' % leeClientDir)

            # 移除各个 Patches 版本目录中的 Temporary 目录
            clientList = self.leeCommon.getRagexeClientList(
                self.leeCommon.patches())
            for x in clientList:
                temporaryDir = self.leeCommon.special(x, 'temporary')
                self.leeCommon.removeDirectory(temporaryDir)

            print('正在删除空目录 ...')
            self.leeCommon.removeEmptyDirectorys(leeClientDir)

            print('已成功重置 LeeClient 客户端环境')
        except Exception as _err:
            print('很抱歉, 重置 LeeClient 客户端环境的过程中发生了意外, 请检查结果')
            raise

    def menufuncSwitchWorkshop(self, clientver):
        '''
        重置工作区, 并切换 LeeClient 到指定的客户端版本
        '''
        if self.patchManager.canRevert():
            self.leeCommon.confirm([
                '在切换版本之前, 需要将 LeeClient 客户端恢复到干净状态',
                '请将自己添加的额外重要文件移出 LeeClient 目录, 避免被程序误删'
            ],
                                   title='切换主程序版本到 %s' % clientver,
                                   prompt='是否立刻执行重置操作?',
                                   inject=self,
                                   cancelExec='inject.menuitemExitAgent()',
                                   evalcmd='inject.menufuncResetWorkshop()')
            print(
                '----------------------------------------------------------------'
            )

        # 先执行与此版本相关的汉化工作
        print('正在汉化 iteminfo ...')
        LeeIteminfoTranslator().doTranslate(clientver)

        print('正在汉化 towninfo ...')
        LeeTowninfoTranslator().doTranslate(clientver)

        print('正在汉化 skillinfolist ...')
        LeeSkillinfolistTranslator().doTranslate(clientver)

        print('正在汉化 skilldescript ...')
        LeeSkilldescriptTranslator().doTranslate(clientver)

        print('正在汉化 客户端按钮 ...')
        LeeButtonTranslator().doTranslate(clientver)

        # 将对应的资源覆盖到 LeeClient 主目录
        print('正在切换版本, 请耐心等待...')
        if not self.patchManager.doApplyPatch(clientver):
            print('很抱歉, 切换仙境传说的主程序到 %s 版本的时发生错误, 请检查结果' % clientver)
        else:
            print('已切换仙境传说的主程序到 %s 版本\r\n' % clientver)

    def menufuncPackageSourceToZipfile(self, packageSourceDirname):
        '''
        将指定的打包源压缩成一个 ZIP 文件
        '''
        leeClientParantDir = self.leeCommon.client('..', withmark=False)
        packageSourceDirpath = '%s/%s' % (leeClientParantDir,
                                          packageSourceDirname)

        zipFilename = LeePublisher().getZipFilename(packageSourceDirpath)
        if not LeePublisher().makeZip(packageSourceDirpath, zipFilename):
            print('很抱歉, 压缩 ZIP 文件时发生错误, 请检查结果')
        else:
            print('已压缩为 ZIP 文件: %s\r\n' % (zipFilename))

    def menufuncPackageSourceToSetup(self, packageSourceDirname):
        '''
        将指定的打包源制作成一个 Setup 安装程序
        '''
        leeClientParantDir = self.leeCommon.client('..', withmark=False)
        packageSourceDirpath = '%s/%s' % (leeClientParantDir,
                                          packageSourceDirname)
        outputDirpath = './Output/%s' % packageSourceDirname

        if not LeePublisher().makeSetup(packageSourceDirpath, outputDirpath):
            print('很抱歉, 制作 Setup 安装程序时发生错误, 请检查结果')
        else:
            print('\r\n已制作完毕的 Setup 安装程序存放在: %s 目录中, 请确认.\r\n' %
                  (os.path.abspath(outputDirpath)))

    def menufuncUpdateButtonTranslateDB(self):
        '''
        根据目前各个客户端的 Resource/Original 目录中的最新文件
        来更新目前正在使用的按钮汉化数据库文件
        '''
        print('正在读取数据库...')
        self.buttonTranslator.load()
        print('正在根据目前 Patches 的内容升级数据库...')
        self.buttonTranslator.update()
        print('正在保存数据库...')
        self.buttonTranslator.save()
        print('更新操作已经完成, 请确认文件的变更内容...\r\n')

    def menufuncClientResourceVerifier(self):
        '''
        对客户端进行资源完整性校验
        '''
        self.leeVerifier.runVerifier()
        print('客户端文件完整性校验已结束\r\n')

    def menufuncBatchDecompileLub(self, lubSourceDirectory):
        '''
        将某个目录下的 lub 文件批量反编译
        需要让用户来选择这个目录的所在位置, 而不是一个固定位置
        '''
        print('您指定的目录为: %s' % lubSourceDirectory)

        if not self.leeCommon.isDirectoryExists(lubSourceDirectory):
            self.leeCommon.exitWithMessage('很抱歉, 你指定的目录不存在, 程序终止')

        if lubSourceDirectory.endswith('/') or lubSourceDirectory.endswith(
                '\\'):
            lubSourceDirectory = lubSourceDirectory[:-1]

        lubOutputDirectory = '%s%s%s' % (
            os.path.dirname(lubSourceDirectory), os.path.sep,
            os.path.basename(lubSourceDirectory) + '_output')
        print('计划的输出目录: %s' % lubOutputDirectory)

        if self.leeCommon.isDirectoryExists(lubOutputDirectory):
            self.leeCommon.exitWithMessage('发现输出目录已经存在, 请先手动删除后重试..')

        print('')
        LeeLua().decodeDir(lubSourceDirectory, lubOutputDirectory)

    def menufuncBatchAmendmentsLub(self):
        '''
        将 Patches 目录下的 lub 文件批量进行整理
        '''
        patchesDir = self.leeCommon.patches()

        for dirpath, _dirnames, filenames in os.walk(patchesDir):
            for filename in filenames:
                fullpath = os.path.normpath('%s/%s' % (dirpath, filename))
                if not filename.lower().endswith('.lub'):
                    continue
                if LeeLua().isTrulyLubFile(fullpath):
                    continue
                if not LeeLua().lubAmendments(fullpath, fullpath):
                    self.leeCommon.exitWithMessage('整理 %s 时发生错误, 请确认.' %
                                                   fullpath)
                else:
                    print('已整理: %s' % os.path.relpath(fullpath, patchesDir))

    def menufuncDetectLubCompiled(self):
        '''
        扫描整个 Patches 目录下的 lub 文件
        检测他们的是否已经被反编译, 并将没有被反编译的 lub 文件列出
        '''
        patchesDir = self.leeCommon.patches()

        print('正在扫描, 可能会花几分钟时间, 请耐心等待...')
        print('')
        work_start = timeit.default_timer()

        for dirpath, _dirnames, filenames in os.walk(patchesDir):
            for filename in filenames:
                fullpath = os.path.normpath('%s/%s' % (dirpath, filename))
                if not filename.lower().endswith('.lub'):
                    continue

                if LeeLua().isTrulyLubFile(fullpath):
                    print('尚未反编译 - %s' %
                          (os.path.relpath(fullpath, patchesDir)))

        work_elapsed = (timeit.default_timer() - work_start)

        print('扫描并检测完毕, 耗时: %0.2f 秒' % work_elapsed)
        print('')

    def menufuncDetectNonAnsiLub(self):
        '''
        扫描整个 Patches 目录下的 lub 文件
        检测他们的文件编码, 并列出非 ANSI 编码的文件
        '''
        patchesDir = self.leeCommon.patches()
        allowANSI = [
            'ASCII', 'EUC-KR', 'LATIN1', 'GBK', 'GB2312', 'CP949',
            'ISO-8859-1', 'WINDOWS-1252'
        ]

        print('正在扫描, 可能会花几分钟时间, 请耐心等待...')
        print('')
        work_start = timeit.default_timer()

        for dirpath, _dirnames, filenames in os.walk(patchesDir):
            for filename in filenames:
                fullpath = os.path.normpath('%s/%s' % (dirpath, filename))
                if not filename.lower().endswith('.lub'):
                    continue

                result, encoding = LeeLua().getLubEncoding(fullpath)
                if result and encoding not in allowANSI:
                    print('%s - %s' %
                          (encoding, os.path.relpath(fullpath, patchesDir)))

        work_elapsed = (timeit.default_timer() - work_start)

        print('扫描并检测完毕, 耗时: %0.2f 秒' % work_elapsed)
        print('')

    def menuitemUpdateButtonTranslateDB(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作 -> 汉化管理 - 更新按钮汉化数据库”时执行
        '''
        self.leeCommon.confirm(
            [
                '已汉化的内容将会被自动继承, 请不用担心',
                '涉及的数据库文件为: Resources/Databases/ButtonTranslate.json'
            ],
            title='更新客户端按钮的翻译数据库',
            prompt='是否执行更新操作?',
            inject=self,
            cancelExec='inject.menuitemExitAgent()',
            evalcmd='inject.menufuncUpdateButtonTranslateDB()')

    def menuitemStatisticsTranslateCoverage(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作 -> 汉化管理 - 汉化覆盖率统计”时执行
        '''
        self.leeCommon.exitWithMessage('此功能目前还在规划中, 待实现...')

    def menuitemClientResourceVerifier(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作 -> 资源管理 - 校验客户端资源完整性”时执行
        '''
        self.leeCommon.confirm(
            ['此过程可以协助排除可能的一些图档丢失情况.', '不过由于需要对客户端的大量文件进行判断, 时间可能会比较长.'],
            title='对客户端资源进行完整性校验',
            prompt='是否确认执行?',
            inject=self,
            cancelExec='inject.menuitemExitAgent()',
            evalcmd='inject.menufuncClientResourceVerifier()')

    def menuitemBatchDecompileLub(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作 -> 脚本管理 - 批量反编译某个目录中的 lub 文件”时执行
        '''
        self.leeCommon.input(
            [
                '您不能指望反编译后的 lua 文件可被 RO 客户端无错运行.',
                '反编译 lua 的函数时, 转换后的结果常出问题(语法错误), 需手动修正.', '',
                '请填写一个包含 lub 文件的 luafiles514 目录的完整路径.',
                '程序只转换后缀为 lub 的文件, 并将反编译后的文件保存到平级目录下.', '',
                '比如你填写的路径是: C:\\luafiles 那么输出会在 C:\\luafiles_output',
                '程序会将无需反编译的文件, 也一起复制到输出目录(保持目录结构).'
            ],
            title='批量反编译指定目录下的 lub 文件',
            prompt='请填写目录路径',
            inject=self,
            evalcmd='inject.menufuncBatchDecompileLub(user_input)')

    def menuitemBatchAmendmentsLub(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作 -> 脚本管理 - 批量整理某个目录中的 lub 文件”时执行
        '''
        self.leeCommon.confirm([
            '此操作会将 Patches 目录中的 lub 文件全部找出',
            '然后移除 lub 文件中的一些多余的注释, 并纠正一些格式错误等等.',
            '',
            '程序只转换后缀为 lub 的文件, 并将处理后的文件直接进行替换.',
            '注意: 由于会直接进行替换, 所以请您一定先自己做好备份! 避免意外.',
        ],
                               title='批量整理所有 lub 文件的内容',
                               prompt='是否立刻执行整理操作?',
                               inject=self,
                               cancelExec='inject.menuitemExitAgent()',
                               evalcmd='inject.menufuncBatchAmendmentsLub()')

    def menuitemDetectLubCompiled(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作 -> 脚本管理 - 扫描并列出所有未被反编译的 lub 文件”时执行
        '''
        self.leeCommon.confirm([
            '此操作会将 Patches 目录中的 lub 文件全部找出',
            '然后判断其是否已经被反编译, 并将没有被反编译的 lub 全部列出.', '',
            '注意: 为了提高效率, 我们只对文件后缀为 lub 的文件进行判断.'
        ],
                               title='扫描并列出所有未被反编译的 lub 文件',
                               prompt='是否立刻执行扫描操作?',
                               inject=self,
                               cancelExec='inject.menuitemExitAgent()',
                               evalcmd='inject.menufuncDetectLubCompiled()')

    def menuitemDetectNonAnsiLub(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作 -> 脚本管理 - 扫描并列出所有非 ANSI 编码的 lub 文件”时执行
        '''
        self.leeCommon.confirm([
            '此操作会将 Patches 目录中的 lub 文件全部找出',
            '然后探测其文件编码, 将所有非 ANSI 类型编码的文件都列出来.', '',
            '注意: GBK 和 EUC-KR 都属于 ANSI 类型编码, 而 UTF8 则不是.',
            '这里的 lub 文件实际上是 lua 的明文脚本文件.'
        ],
                               title='扫描并列出所有非 ANSI 编码的 lub 文件',
                               prompt='是否立刻执行扫描操作?',
                               inject=self,
                               cancelExec='inject.menuitemExitAgent()',
                               evalcmd='inject.menufuncDetectNonAnsiLub()')

    def menuitemBuildSourceUseGRF(self):
        '''
        菜单处理函数
        当选择“生成 / 打包 / 制作客户端安装程 -> 将当前客户端状态导出成打包源 ->
        将 data 目录压缩成 GRF (推荐)”时执行
        '''
        LeePublisher().makeSource(True)

    def menuitemBuildSourceDontUseGRF(self):
        '''
        菜单处理函数
        当选择“生成 / 打包 / 制作客户端安装程 -> 将当前客户端状态导出成打包源 ->
        不压缩 data 目录中的文件, 保持零散小文件状态 (不推荐)”时执行
        '''
        LeePublisher().makeSource(False)

    def menuitemConfrimDataFolderType(self):
        '''
        菜单处理函数
        当选择“生成 / 打包 / 制作客户端安装程 -> 将当前客户端状态导出成打包源”时执行
        '''
        self.leeCommon.menu([[
            '将 data 目录压缩成 GRF (推荐, 仅 Windows 支持)',
            'inject.menuitemBuildSourceUseGRF()'
        ],
                             [
                                 '不压缩 data 目录中的文件, 保持零散小文件状态 (不推荐)',
                                 'inject.menuitemBuildSourceDontUseGRF()'
                             ]],
                            title='生成打包源时, 您希望如何处理 data 目录:',
                            inject=self)

    def menuitemPackageSourceToZipfile(self):
        '''
        菜单处理函数
        当选择“生成 / 打包 / 制作客户端安装程 -> 选择一个打包源, 压缩成 ZIP 包”时执行
        '''
        packageSourceDirnameList = LeePublisher().getPackageSourceList(
            self.leeCommon.client('..'))
        if packageSourceDirnameList is None:
            self.leeCommon.exitWithMessage('很抱歉, 无法获取打包源列表, 程序终止')

        if not packageSourceDirnameList:
            self.leeCommon.exitWithMessage('没有发现任何可用的打包源, 请先生成一个吧')

        self.leeCommon.menu(
            [[x, 'inject.menufuncPackageSourceToZipfile(\'%s\')' % x]
             for x in packageSourceDirnameList],
            title='将指定的打包源压缩成 ZIP 文件',
            prompt='请选择你想压缩的打包源目录',
            inject=self,
            cancelExec='inject.entrance()',
            withCancel=True)

    def menuitemPackageSourceToSetup(self):
        '''
        菜单处理函数
        当选择“生成 / 打包 / 制作客户端安装程 -> 选择一个打包源, 制作游戏安装程序”时执行
        '''
        packageSourceDirnameList = LeePublisher().getPackageSourceList(
            self.leeCommon.client('..'))
        if packageSourceDirnameList is None:
            self.leeCommon.exitWithMessage('很抱歉, 无法获取打包源列表, 程序终止')

        if not packageSourceDirnameList:
            self.leeCommon.exitWithMessage('没有发现任何可用的打包源, 请先生成一个吧')

        self.leeCommon.menu(
            [[x, 'inject.menufuncPackageSourceToSetup(\'%s\')' % x]
             for x in packageSourceDirnameList],
            title='将指定的打包源制作成安装程序',
            prompt='请选择你想制作的打包源目录',
            inject=self,
            cancelExec='inject.entrance()',
            withCancel=True)

    def menuitemSwitchWorkshop(self):
        '''
        菜单处理函数
        当选择“切换客户端到指定版本”时执行
        '''
        clientList = self.leeCommon.getRagexeClientList(
            self.leeCommon.patches())
        if clientList is None:
            self.leeCommon.exitWithMessage('很抱歉, 无法获取客户端版本列表, 程序终止')

        menus = [[
            '切换到 %s 版本' % x,
            'inject.menufuncSwitchWorkshop(\'%s\')' % x
        ] for x in clientList]
        menus.insert(0, ['将客户端重置回干净状态', 'inject.menuitemResetWorkshop()'])

        self.leeCommon.menu(items=menus,
                            title='切换客户端到指定版本, 以便与服务端配套工作:',
                            inject=self,
                            cancelExec='inject.entrance()',
                            withCancel=True)

    def menuitemResetWorkshop(self):
        '''
        菜单处理函数
        当选择“切换客户端到指定版本 -> 将客户端重置回干净状态”时执行
        '''
        if self.patchManager.canRevert():
            self.leeCommon.confirm([
                '此操作可以将 LeeClient 客户端恢复到干净状态',
                '请将自己添加的额外重要文件移出 LeeClient 目录, 避免被程序误删.', '',
                '提醒: 此操作不会删除 Utility/Import 目录下的文件, 请放心.'
            ],
                                   title='将客户端重置回干净状态',
                                   prompt='是否立刻执行重置操作?',
                                   inject=self,
                                   cancelExec='inject.menuitemExitAgent()',
                                   evalcmd='inject.menufuncResetWorkshop()')
        else:
            self.leeCommon.exitWithMessage('您的客户端环境看起来十分干净, 不需要再进行清理了.')

    def menuitemMaintenance(self):
        '''
        菜单处理函数
        当选择“进行一些开发者维护工作”时执行
        '''
        self.leeCommon.menu(
            [['汉化管理 - 更新按钮汉化数据库', 'inject.menuitemUpdateButtonTranslateDB()'],
             [
                 '汉化管理 - 汉化覆盖率统计(计划实现)',
                 'inject.menuitemStatisticsTranslateCoverage()'
             ],
             ['资源管理 - 校验客户端资源完整性', 'inject.menuitemClientResourceVerifier()'],
             [
                 '脚本管理 - 批量反编译某个目录中的 lub 文件',
                 'inject.menuitemBatchDecompileLub()'
             ],
             [
                 '脚本管理 - 批量整理某个目录中的 lub 文件',
                 'inject.menuitemBatchAmendmentsLub()'
             ],
             [
                 '脚本管理 - 扫描并列出所有未被反编译的 lub 文件',
                 'inject.menuitemDetectLubCompiled()'
             ],
             [
                 '脚本管理 - 扫描并列出所有非 ANSI 编码的 lub 文件',
                 'inject.menuitemDetectNonAnsiLub()'
             ]],
            title='以下是一些开发者维护工作, 请选择您需要的操作:',
            inject=self)

    def menuitemMakePackageOrSetup(self):
        '''
        菜单处理函数
        当选择“生成 / 打包 / 制作客户端安装程序”时执行
        '''
        self.leeCommon.menu(
            [['将当前客户端状态导出成打包源', 'inject.menuitemConfrimDataFolderType()'],
             ['选择一个打包源, 压缩成 ZIP 包', 'inject.menuitemPackageSourceToZipfile()'],
             [
                 '选择一个打包源, 制作游戏安装程序 (仅 Windows 平台支持)',
                 'inject.menuitemPackageSourceToSetup()'
             ]],
            title='生成 / 打包 / 制作客户端安装程序, 请选择您需要的操作:',
            inject=self)

    def menuitemExitAgent(self):
        '''
        菜单处理函数
        当选择“退出程序”时执行
        '''
        self.leeCommon.exitWithMessage('感谢您的使用, 再见')

    def entrance(self):
        '''
        菜单处理函数
        这里是主菜单的入口处
        '''
        self.leeCommon.menu(
            [['切换客户端到指定版本', 'inject.menuitemSwitchWorkshop()'],
             ['生成 / 打包 / 制作客户端安装程序', 'inject.menuitemMakePackageOrSetup()'],
             ['进行一些开发者维护工作', 'inject.menuitemMaintenance()'],
             ['退出程序', 'inject.menuitemExitAgent()']],
            title='您好, 欢迎使用 LeeClientAgent 来管理您的客户端!',
            inject=self)
Beispiel #4
0
class LeePatchManager:
    '''
    用于管理补丁文件列表的操作类
    '''
    class SourceFileNotFoundError(FileNotFoundError):
        pass

    def __init__(self):
        self.leeCommon = LeeCommon()
        self.stagingFiles = []
        self.patchesFiles = []
        self.backupFiles = []

        self.forceRemoveDirs = [
            'AI', 'AI_sakray', '_tmpEmblem', 'memo', 'Replay', 'SaveData',
            'Navigationdata', 'System'
        ]
        return

    def __getSessionPath(self):
        '''
        获取最后一次应用补丁时,路径信息数据库的存储路径
        '''
        revertDir = self.leeCommon.resources('Databases/RevertData')
        os.makedirs(revertDir, exist_ok=True)
        sessionInfoFile = os.path.abspath('%s/LeeClientRevert.json' %
                                          revertDir)
        return sessionInfoFile

    def __createSession(self):
        self.stagingFiles.clear()
        self.backupFiles.clear()
        self.patchesFiles.clear()

    def __pathrestore(self, dictobj):
        '''
        用于处理 dictobj 字典对象中存储的路径
        把反斜杠转换回当前系统的路径分隔符
        '''
        dictobj['src'] = dictobj['src'].replace('\\', os.path.sep)
        dictobj['dst'] = dictobj['dst'].replace('\\', os.path.sep)
        return dictobj

    def __pathnorm(self, dictobj):
        '''
        用于处理 dictobj 字典对象中存储的路径
        把斜杠转换回统一的反斜杠
        '''
        dictobj['src'] = dictobj['src'].replace('/', '\\')
        dictobj['dst'] = dictobj['dst'].replace('/', '\\')
        return dictobj

    def __loadSession(self):
        sessionInfoFile = self.__getSessionPath()
        if os.path.exists(sessionInfoFile) and os.path.isfile(sessionInfoFile):
            self.backupFiles.clear()
            self.patchesFiles.clear()

            patchesInfo = json.load(
                open(sessionInfoFile, 'r', encoding='utf-8'))
            self.backupFiles = patchesInfo['backuplist']
            self.patchesFiles = patchesInfo['patchlist']

            # 标准化处理一下反斜杠, 以便在跨平台备份恢复时可以顺利找到文件
            self.backupFiles = [
                filepath.replace('\\', os.path.sep)
                for filepath in self.backupFiles
            ]
            self.patchesFiles = [
                self.__pathrestore(item) for item in self.patchesFiles
            ]

            return True
        return False

    def __copyDirectory(self, srcDirectory):
        scriptDir = self.leeCommon.utility()
        useless_files = ['thumbs.db', '.ds_store', '.gitignore', '.gitkeep']

        # 复制文件,且记录所有可能会应用的目的地址
        for dirpath, _dirnames, filenames in os.walk(srcDirectory):
            for filename in filenames:
                if filename.lower() in useless_files:
                    continue
                file_path = os.path.join(dirpath, filename)
                srcrel_path = os.path.relpath(file_path, scriptDir)
                dstrel_path = os.path.relpath(file_path, srcDirectory)
                self.stagingFiles.append({
                    'src': srcrel_path,
                    'dst': dstrel_path
                })

    def __commitSession(self):
        scriptDir = self.leeCommon.utility(withmark=False)
        leeClientDir = self.leeCommon.client(withmark=False)
        backupDir = self.leeCommon.patches('Backup')

        try:
            # 确保来源文件都存在
            for item in self.stagingFiles:
                src_path = os.path.abspath('%s/%s' % (scriptDir, item['src']))
                if not (os.path.exists(src_path) and os.path.isfile(src_path)):
                    raise self.SourceFileNotFoundError(
                        "Can't found source patch file : %s" % src_path)

            # 备份所有可能会被覆盖的文件,并记录备份成功的文件
            for item in self.stagingFiles:
                dst_path = os.path.abspath('%s/%s' %
                                           (leeClientDir, item['dst']))
                dst_dirs = os.path.dirname(dst_path)

                try:
                    # 目的地文件已经存在,需要备份避免误删
                    if os.path.exists(dst_path) and os.path.isfile(dst_path):
                        backup_path = os.path.abspath('%s/%s' %
                                                      (backupDir, item['dst']))
                        backup_dirs = os.path.dirname(backup_path)
                        os.makedirs(backup_dirs, exist_ok=True)
                        shutil.copyfile(dst_path, backup_path)
                        self.backupFiles.append(item['dst'])
                except BaseException as err:
                    print('文件备份失败 : %s' % dst_path)
                    raise err

            # 执行文件复制工作,并记录复制成功的文件
            for item in self.stagingFiles:
                src_path = os.path.abspath('%s/%s' % (scriptDir, item['src']))
                dst_path = os.path.abspath('%s/%s' %
                                           (leeClientDir, item['dst']))
                dst_dirs = os.path.dirname(dst_path)

                os.makedirs(dst_dirs, exist_ok=True)
                if os.path.exists(dst_path):
                    os.remove(dst_path)
                shutil.copyfile(src_path, dst_path)
                # print('复制 %s 到 %s' % (src_path, dst_path))
                self.patchesFiles.append(item)

        except BaseException as err:
            # 根据 self.backupFiles 和 self.patchesFiles 记录的信息回滚后报错
            print('_commitSession 失败, 正在回滚...')
            if self.doRevertPatch(loadSession=False):
                print('回滚成功, 请检查目前的文件状态是否正确')
            else:
                print('很抱歉, 回滚失败了, 请检查目前的文件状态')
            return False

        # 记录备份和成功替换的文件信息
        sessionInfoFile = self.__getSessionPath()
        if os.path.exists(sessionInfoFile):
            os.remove(sessionInfoFile)

        # 标准化处理一下反斜杠, 以便在跨平台备份恢复时可以顺利找到文件
        self.backupFiles = [
            filepath.replace('/', '\\') for filepath in self.backupFiles
        ]
        self.patchesFiles = [
            self.__pathnorm(item) for item in self.patchesFiles
        ]

        json.dump(
            {
                'patchtime': time.strftime('%Y-%m-%d %H:%M:%S',
                                           time.localtime()),
                'backuplist': self.backupFiles,
                'patchlist': self.patchesFiles
            },
            open(sessionInfoFile, 'w', encoding='utf-8'),
            ensure_ascii=False,
            indent=4)

        return True

    def canRevert(self):
        self.__loadSession()
        leeClientDir = self.leeCommon.client()

        restoreFileInfo = []

        for folder in self.forceRemoveDirs:
            for dirpath, _dirnames, filenames in os.walk(
                    os.path.join(leeClientDir, folder)):
                for filename in filenames:
                    fullpath = os.path.join(dirpath, filename)
                    restoreFileInfo.append(
                        [1, os.path.relpath(fullpath, leeClientDir)])

        for item in self.patchesFiles:
            path = item['dst']
            restoreFileInfo.append([1, path])

        return len(restoreFileInfo) > 0

    def doRevertPatch(self, loadSession=True):
        if loadSession:
            self.__loadSession()

        leeClientDir = self.leeCommon.client(withmark=False)
        backupDir = self.leeCommon.patches('Backup')
        sessionInfoFile = self.__getSessionPath()

        # 删除之前应用的文件
        for item in self.patchesFiles:
            abspath = os.path.abspath('%s/%s' % (leeClientDir, item['dst']))
            if os.path.exists(abspath) and os.path.isfile(abspath):
                # print('即将删除 : %s' % abspath)
                os.remove(abspath)

        # 删除一些需要强制移除的目录
        for folder in self.forceRemoveDirs:
            for dirpath, _dirnames, filenames in os.walk(
                    self.leeCommon.client(folder)):
                for filename in filenames:
                    fullpath = os.path.join(dirpath, filename)
                    if os.path.exists(fullpath) and os.path.isfile(fullpath):
                        # print('强制移除 : %s' % fullpath)
                        os.remove(fullpath)

        # 还原之前备份的文件列表
        for item in self.backupFiles:
            backup_path = os.path.abspath('%s/%s' % (backupDir, item))
            source_path = os.path.abspath('%s/%s' % (leeClientDir, item))
            shutil.copyfile(backup_path, source_path)

        if os.path.exists(backupDir) and os.path.isdir(backupDir):
            shutil.rmtree(backupDir)

        if os.path.exists(sessionInfoFile) and os.path.isfile(sessionInfoFile):
            os.remove(sessionInfoFile)

        self.leeCommon.removeEmptyDirectorys(leeClientDir)
        return True

    def doApplyPatch(self, clientver):
        '''
        应用特定版本的补丁
        '''
        clientList = self.leeCommon.getRagexeClientList(
            self.leeCommon.patches())

        if not clientver in clientList:
            self.leeCommon.exitWithMessage('您期望切换的版本号 %s 是无效的' % clientver)

        beforeDir = self.leeCommon.special(None, 'patches_before')
        ragexeDir = self.leeCommon.special(clientver, 'build')
        clientOriginDir = self.leeCommon.special(clientver, 'origin')
        clientTranslatedDir = self.leeCommon.special(clientver, 'translated')
        clientTemporaryDir = self.leeCommon.special(clientver, 'temporary')
        afterDir = self.leeCommon.special(None, 'patches_after')

        importBeforeDir = self.leeCommon.special(None, 'import_before')
        importClientDir = self.leeCommon.special(clientver, 'import_version')
        importAftertDir = self.leeCommon.special(None, 'import_after')

        # 确认对应的资源目录在是存在的
        if not self.leeCommon.isDirectoryExists(beforeDir):
            self.leeCommon.exitWithMessage('无法找到 BeforePatches 的 Base 目录: %s' %
                                           beforeDir)
        if not self.leeCommon.isDirectoryExists(ragexeDir):
            self.leeCommon.exitWithMessage('无法找到 %s 版本的 Ragexe 目录: %s' %
                                           (clientver, ragexeDir))
        if not self.leeCommon.isDirectoryExists(clientOriginDir):
            self.leeCommon.exitWithMessage('无法找到 %s 版本的 Original 目录: %s' %
                                           (clientver, clientOriginDir))
        if not self.leeCommon.isDirectoryExists(clientTranslatedDir):
            self.leeCommon.exitWithMessage('无法找到 %s 版本的 Translated 目录: %s' %
                                           (clientver, clientTranslatedDir))
        if not self.leeCommon.isDirectoryExists(afterDir):
            self.leeCommon.exitWithMessage('无法找到 AfterPatches 的 Base 目录: %s' %
                                           afterDir)

        if not self.leeCommon.isDirectoryExists(importBeforeDir):
            self.leeCommon.exitWithMessage(
                '无法找到 BeforePatches 的 Import 目录: %s' % importBeforeDir)
        if not self.leeCommon.isDirectoryExists(importClientDir):
            self.leeCommon.exitWithMessage('无法找到 %s 版本的 Import 目录: %s' %
                                           (clientver, importClientDir))
        if not self.leeCommon.isDirectoryExists(importAftertDir):
            self.leeCommon.exitWithMessage(
                '无法找到 AfterPatches 的 Import 目录: %s' % importAftertDir)

        # 创建一个事务并执行复制工作, 最后提交事务
        self.__createSession()
        self.__copyDirectory(beforeDir)
        self.__copyDirectory(importBeforeDir)  # Import
        self.__copyDirectory(ragexeDir)
        self.__copyDirectory(clientOriginDir)
        self.__copyDirectory(clientTemporaryDir)
        self.__copyDirectory(clientTranslatedDir)
        self.__copyDirectory(importClientDir)  # Import
        self.__copyDirectory(afterDir)
        self.__copyDirectory(importAftertDir)  # Import

        if not self.__commitSession():
            print('应用特定版本的补丁过程中发生错误, 终止...')
            return False

        return True
Beispiel #5
0
class LeeGrf:
    def __init__(self):
        self.leeCommon = LeeCommon()
        self.patchManager = LeePatchManager()

    def isGrfExists(self):
        leeClientDir = self.leeCommon.client(withmark=False)
        grfFiles = glob.glob('%s/*.grf' % leeClientDir)
        return len(grfFiles) > 0

    def makeGrf(self, dataDirpath, grfOutputPath):
        # 确认操作系统平台
        if platform.system() != 'Windows':
            self.leeCommon.exitWithMessage('很抱歉, 此功能目前只能在 Windows 平台上运行.')

        # 确认 GrfCL 所需要的 .net framework 已安装
        if not self.leeCommon.isDotNetFrameworkInstalled('v3.5'):
            print('您必须先安装微软的 .NET Framework v3.5 框架.')
            self.leeCommon.exitWithMessage(
                '下载地址: https://www.microsoft.com/zh-CN/download/details.aspx?id=21'
            )

        # 确认已经切换到了需要的客户端版本
        if not self.patchManager.canRevert():
            self.leeCommon.exitWithMessage(
                '请先将 LeeClient 切换到某个客户端版本, 以便制作出来的 grf 文件内容完整.')

        # 确认有足够的磁盘剩余空间进行压缩
        currentDriver = self.leeCommon.utility()[0]
        currentFreeSpace = self.leeCommon.getDiskFreeSpace(currentDriver)
        if currentFreeSpace <= 1024 * 1024 * 1024 * 3:
            self.leeCommon.exitWithMessage('磁盘 %s: 的空间不足 3GB, 请清理磁盘释放更多空间.' %
                                           currentDriver)

        # 确认 GrfCL 文件存在
        scriptDir = self.leeCommon.utility(withmark=False)
        grfCLFilepath = ('%s/Bin/GrfCL/GrfCL.exe' % scriptDir).replace(
            '/', os.path.sep)
        if not self.leeCommon.isFileExists(grfCLFilepath):
            self.leeCommon.exitWithMessage(
                '制作 grf 文件所需的 GrfCL.exe 程序不存在, 无法执行压缩.')

        # data.grf 文件若存在则进行覆盖确认
        if self.leeCommon.isFileExists(grfOutputPath):
            lines = [
                '发现客户端目录中已存在 %s 文件,' % os.path.basename(grfOutputPath),
                '若继续将会先删除此文件, 为避免文件被误删, 请您进行确认.'
            ]
            title = '文件覆盖提示'
            prompt = '是否删除文件并继续?'
            if not self.leeCommon.confirm(lines, title, prompt, None, None,
                                          None):
                self.leeCommon.exitWithMessage('由于您放弃继续, 程序已自动终止.')
            os.remove(grfOutputPath)

        # 执行压缩工作(同步等待)
        grfCLProc = subprocess.Popen(
            '%s %s' %
            (grfCLFilepath, '-breakOnExceptions true -makeGrf "%s" "%s"' %
             (os.path.relpath(grfOutputPath, os.path.dirname(grfCLFilepath)),
              os.path.relpath(dataDirpath, os.path.dirname(grfCLFilepath)))),
            stdout=sys.stdout,
            cwd=os.path.dirname(grfCLFilepath))
        grfCLProc.wait()

        # 确认结果并输出提示信息表示压缩结束
        if grfCLProc.returncode == 0 and self.leeCommon.isFileExists(
                grfOutputPath):
            print('已经将 data 目录压缩为 data.grf 并存放到根目录.')
            print('')
            self.leeCommon.printSmallCutLine()
            print('')
        else:
            self.leeCommon.exitWithMessage('进行压缩工作的时候发生错误, 请发 Issue 进行反馈.')
Beispiel #6
0
class LeePublisher:
    def __init__(self):
        self.leeCommon = LeeCommon()
        self.leeConfig = LeeConfigure()
        self.patchManager = LeePatchManager()

    def removeOldGrf(self):
        leeClientDir = self.leeCommon.client(withmark=False)
        grfFiles = glob.glob('%s/*.grf' % leeClientDir)

        for filepath in grfFiles:
            os.remove(filepath)

    def ensureHasGRF(self):
        leeClientDir = self.leeCommon.client(withmark=False)

        if LeeGrf().isGrfExists():
            reUseExistsGrfFiles = self.leeCommon.confirm(
                [
                    '当前客户端目录已经存在了 Grf 文件', '请问是直接使用他们(y), 还是需要重新生成(n)?', '',
                    '注意: 若选重新生成(n), 那么目前的 Grf 文件将被立刻删除.'
                ],
                title='文件覆盖确认',
                prompt='是否继续使用这些 Grf 文件?',
                inject=self,
                cancelExec='inject.removeOldGrf()',
                evalcmd=None)

            if reUseExistsGrfFiles:
                return

        LeeGrf().makeGrf('%s/data' % leeClientDir,
                         '%s/data.grf' % leeClientDir)

        if not LeeGrf().isGrfExists():
            self.leeCommon.exitWithMessage(
                '请先将 data 目录打包为 Grf 文件, 以便提高文件系统的复制效率.')

    def makeSource(self, useGrf):
        '''
        将 LeeClient 的内容复制到打包源目录(并删除多余文件)
        '''
        leeClientDir = self.leeCommon.client(withmark=False)
        packageSourceCfg = self.leeConfig.get('PackageSource')

        # 判断是否已经切换到某个客户端版本
        if not self.patchManager.canRevert():
            self.leeCommon.exitWithMessage(
                '请先将 LeeClient 切换到某个客户端版本, 否则制作出来的 grf 内容不完整.')

        if useGrf:
            self.ensureHasGRF()

        # 判断磁盘的剩余空间是否足够
        currentDriver = self.leeCommon.utility()[0]
        currentFreeSpace = self.leeCommon.getDiskFreeSpace(currentDriver)
        if currentFreeSpace <= 1024 * 1024 * 1024 * 4:
            self.leeCommon.exitWithMessage('磁盘 %s: 的空间不足 4 GB, 请清理磁盘释放更多空间.' %
                                           currentDriver)

        # 生成一个 LeeClient 平级的发布目录
        nowTime = time.strftime("%Y%m%d_%H%M%S", time.localtime())
        releaseDirName = 'LeeClient_Release_%s' % nowTime
        releaseDirpath = self.leeCommon.client('../' + releaseDirName,
                                               withmark=False)

        # 先列出需要复制到打包源的文件列表
        filterDirectories = ['Utility', '.git', '.vscode']
        filterFiles = ['.gitignore', '.DS_Store']

        # 若使用 grf 文件的话, 则排除掉 data 目录
        if useGrf:
            filterDirectories.append('data')

        print('正在分析需要复制的文件, 请稍后...')
        copyFileList = []
        for dirpath, dirnames, filenames in os.walk(leeClientDir):
            for filename in filenames:
                fullpath = os.path.join(dirpath, filename)

                # 过滤一下不需要导出的目录 (大小写敏感)
                dirnames[:] = [
                    d for d in dirnames if d not in filterDirectories
                ]

                # 过滤一下不需要导出的文件 (不区分大小写)
                isBlocked = False
                for filterFile in filterFiles:
                    if filterFile.lower() in filename.lower():
                        isBlocked = True
                        break
                if isBlocked:
                    continue

                # 判断是否需要移除调试版的登录器主程序
                if filename.lower().endswith(
                        '.exe') and '_ReportError.exe' in filename:
                    if packageSourceCfg['AutoRemoveDebugClient']:
                        continue

                # 记录到 copyFileList 表示需要复制此文件到打包源
                copyFileList.append(fullpath)

        print('分析完毕, 共需复制 %d 个文件, 马上开始.' % len(copyFileList))

        # 确定游戏启动入口的相对路径
        srcClientName = packageSourceCfg['SourceClientName']
        pubClientName = packageSourceCfg['PublishClientName']

        if packageSourceCfg['SourceClientName'] == 'auto':
            for srcFilepath in copyFileList:
                if not srcFilepath.lower().endswith('.exe'):
                    continue
                filename = os.path.basename(srcFilepath)
                if '_patched.exe' in filename:
                    srcClientName = filename
                    break

        # 把文件拷贝到打包源
        # TODO: 最好能够显示文件的复制进度
        # http://zzq635.blog.163.com/blog/static/1952644862013125112025129/
        for srcFilepath in copyFileList:
            # 获取相对路径, 用于复制到目标文件时使用
            relFilepath = os.path.relpath(srcFilepath, leeClientDir)

            # 构建复制到的目标文件全路径
            dstFilepath = '%s/%s' % (releaseDirpath, relFilepath)

            # 对游戏的入口程序进行重命名操作
            bIsSourceClient = False
            if srcFilepath.lower().endswith('.exe'):
                if os.path.basename(srcFilepath) == srcClientName:
                    bIsSourceClient = True
                    dstFilepath = self.leeCommon.replaceBasename(
                        dstFilepath, pubClientName)

            print('正在复制: %s%s' % (relFilepath, ' (%s)' %
                                  pubClientName if bIsSourceClient else ''))
            os.makedirs(os.path.dirname(dstFilepath), exist_ok=True)
            shutil.copyfile(srcFilepath, dstFilepath)

        # 把最终发布源所在的目录当做参数返回值回传
        return releaseDirpath

    def getZipFilename(self, sourceDir):
        if sourceDir.endswith('\\') or sourceDir.endswith('/'):
            sourceDir = sourceDir[:-1]
        return '%s.zip' % sourceDir

    def getPackageSourceList(self, dirpath):
        '''
        根据指定的 dir 中枚举出子目录的名字 (即打包源的目录名)
        返回: Array 保存着每个子目录名称的数组
        '''
        dirlist = []
        osdirlist = os.listdir(dirpath)

        for dname in osdirlist:
            if not dname.lower().startswith('leeclient_release_'):
                continue
            if os.path.isdir(os.path.normpath(dirpath) + os.path.sep + dname):
                dirlist.append(dname)

        dirlist.sort()
        return dirlist

    def makeZip(self, sourceDir, zipSavePath):
        '''
        将打包源目录直接压缩成一个 zip 文件
        '''
        # https://blog.csdn.net/dou_being/article/details/81546172
        # https://blog.csdn.net/zhd199500423/article/details/80853405

        zipCfg = self.leeConfig.get('ZipConfigure')
        return LeeZipfile().zip(sourceDir, zipSavePath,
                                zipCfg['TopLevelDirName'])

    def makeSetup(self, sourceDir, setupOutputDir=None):
        '''
        将打包源目录直接制作成一个 Setup 安装程序
        '''
        if setupOutputDir is None:
            setupOutputDir = './Output'

        sourceDir = os.path.abspath(sourceDir)
        setupOutputDir = os.path.abspath(setupOutputDir)

        # 判断 InnoSetup 是否已经安装
        if not self.__isInnoSetupInstalled():
            # 若未安装则进行安装 (此处需要管理员权限)
            if self.__instInnoSetup():
                # 安装后将补丁文件复制到 InnoSetup 的安装目录中
                self.__applyInnoSetupLdrPatch()
            else:
                self.leeCommon.exitWithMessage('无法成功安装 Inno Setup, 请联系作者进行排查')

        # 再次进行环境检查, 确保一切就绪
        if not self.__checkInnoSetupStatus():
            self.leeCommon.exitWithMessage('本机的 Inno Setup 环境不正确, 无法继续进行工作.')

        # 读取目前的配置值, 请求用户确认后继续
        configure = self.__choiceConfigure()
        if configure is None:
            self.leeCommon.exitWithMessage('您没有确定用于生成 Setup 的配置, 无法继续进行工作.')

        # 若是第一次使用, 则需要帮用户生成一个 GUID 并嘱咐用户保存 GUID
        if str(configure['LeeAppId']).lower() == 'none':
            # 需要帮用户初始化一个 AppId 并告知用户记录好这个值
            print('发现配置 “%s” 的 LeeAppId 值为 None' % configure['LeeName'])
            print('您必须为其分配一个 GUID. 程序已为您自动生成了一个 GUID:')
            print('')
            print(str(uuid.uuid1()).upper())
            print('')
            print('请复制后粘贴到 LeeClientAgent.yml 文件中 SetupConfigure 节点的')
            print('对应选项里面替换掉 None 值, 然后再重试一次.')
            print('')
            self.leeCommon.pauseScreen()
            sys.exit(0)

        # 在 configure 中补充其他参数
        configure['LeePackageSourceDirpath'] = sourceDir
        configure['LeeOutputDir'] = setupOutputDir
        configure['LeeAppId'] = (r'{{%s}' % configure['LeeAppId']).upper()

        # 确认打包源中配置里填写的“主程序”和“游戏设置程序”都存在, 不在则中断
        leeAppExePath = os.path.abspath(
            '%s/%s' % (sourceDir, configure['LeeAppExeName']))
        leeGameSetupExePath = os.path.abspath(
            '%s/%s' % (sourceDir, configure['LeeGameSetupExeName']))
        if not self.leeCommon.isFileExists(leeAppExePath):
            self.leeCommon.exitWithMessage(
                'LeeAppExeName 指向的主程序 %s 不在打包源目录中: %s' %
                (configure['LeeAppExeName'], sourceDir))
        if not self.leeCommon.isFileExists(leeGameSetupExePath):
            self.leeCommon.exitWithMessage(
                'leeGameSetupExePath 指向的主程序 %s 不在打包源目录中: %s' %
                (configure['LeeGameSetupExeName'], sourceDir))

        # 读取脚本模板
        scriptTemplateContent = self.__readScriptTemplate()

        # 根据配置进行配置项的值替换
        scriptFinallyContent = self.__generateFinallyScript(
            scriptTemplateContent, configure)

        # 将最终的脚本保存到临时目录中
        scriptCachePath = self.__saveFinallyScriptToCache(scriptFinallyContent)

        # 调用 ISCC.exe 执行 Setup 的打包操作
        return self.__runInnoScript(scriptCachePath)

    def __isInnoSetupInstalled(self):
        '''
        判断 Inno Setup 是否已经安装到电脑中
        '''
        innoSetupDir = self.__getInnoSetupInstallPath()
        if innoSetupDir is None:
            return False
        return self.leeCommon.isFileExists('%sCompil32.exe' % innoSetupDir)

    def __getInnoSetupInstallPath(self):
        '''
        获取 Inno Setup 的安装目录, 末尾自动补斜杠
        '''
        try:
            if platform.system() != 'Windows':
                self.leeCommon.exitWithMessage(
                    '很抱歉, %s 此函数目前只能在 Windows 平台上运行.' %
                    sys._getframe().f_code.co_name)

            # 根据不同的平台切换注册表路径
            if platform.machine() == 'AMD64':
                innoSetup_key = winreg.OpenKey(
                    winreg.HKEY_LOCAL_MACHINE,
                    'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Inno Setup 5_is1'
                )
            else:
                innoSetup_key = winreg.OpenKey(
                    winreg.HKEY_LOCAL_MACHINE,
                    'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Inno Setup 5_is1'
                )

            # 读取 Inno Setup 的安装目录
            installLocation, _value_type = winreg.QueryValueEx(
                innoSetup_key, 'InstallLocation')
            if not (installLocation.endswith('\\')
                    or installLocation.endswith('/')):
                installLocation = '%s%s' % (installLocation, os.path.sep)

            return installLocation
        except Exception as _err:
            return None

    def __applyInnoSetupLdrPatch(self):
        '''
        应用 SetupLdr.e32 补丁到 Inno Setup 的安装目录下 (要求管理员权限)
        '''
        # 此过程要求管理员权限, 看看如何检测一下
        if not self.leeCommon.isAdministrator():
            self.leeCommon.exitWithMessage('此操作要求程序“以管理员权限运行”, 请重试.')

        scriptDir = self.leeCommon.utility(withmark=False)
        innoSetupDir = self.__getInnoSetupInstallPath()

        if innoSetupDir is None:
            return False

        srcFilepath = ('%s/Bin/InnoSetup/Resources/Installer/SetupLdr.e32' %
                       scriptDir).replace('/', os.path.sep)
        dstFilepath = ('%sSetupLdr.e32' % innoSetupDir)
        bakFilepath = ('%sSetupLdr.e32.bak' % innoSetupDir)

        if (not self.leeCommon.isFileExists(bakFilepath)
            ) and self.leeCommon.isFileExists(dstFilepath):
            shutil.copyfile(dstFilepath, bakFilepath)

        if not self.leeCommon.isFileExists(srcFilepath):
            return False
        os.remove(dstFilepath)
        shutil.copyfile(srcFilepath, dstFilepath)

        return True

    def __checkInnoSetupStatus(self):
        '''
        检查 Inno Setup 的状态是否正常且符合要求
        '''
        innoSetupDir = self.__getInnoSetupInstallPath()
        if innoSetupDir is None:
            return False

        # Inno Setup 是否已经安装
        if not self.__isInnoSetupInstalled():
            return False

        # Inno Setup 的 ISCC.exe 是否存在
        if not self.leeCommon.isFileExists('%sISCC.exe' % innoSetupDir):
            return False

        # 是否已经安装了 SetupLdr.e32 补丁
        setupLdrMD5 = self.leeCommon.getMD5ForSmallFile('%sSetupLdr.e32' %
                                                        innoSetupDir)
        if setupLdrMD5 != '544dbcf30c8ccb55082709b095173f6c':
            return False

        return True

    def __instInnoSetup(self):
        '''
        安装 Inno Setup 并确保安装成功 (要求管理员权限)
        '''
        # 此过程要求管理员权限, 在此进行检查
        if not self.leeCommon.isAdministrator():
            self.leeCommon.exitWithMessage('此操作要求程序“以管理员权限运行”, 请重试.')

        # 先确认 Inno Setup 的安装程序是否存在
        scriptDir = self.leeCommon.utility(withmark=False)
        installerFilepath = (
            '%s/Bin/InnoSetup/Resources/Installer/innosetup-5.6.1-unicode.exe'
            % scriptDir).replace('/', os.path.sep)
        if not self.leeCommon.isFileExists(installerFilepath):
            return False

        # 执行静默安装过程
        setupProc = subprocess.Popen('%s /VERYSILENT' % installerFilepath,
                                     stdout=sys.stdout,
                                     cwd=os.path.dirname(installerFilepath))
        setupProc.wait()

        # 确认结果并输出提示信息表示压缩结束
        return setupProc.returncode == 0 and self.__isInnoSetupInstalled()

    def choiceExit(self):
        print('不选择任何一个配置的话, 无法继续工作, 请重试.')

    def __choiceConfigure(self):
        '''
        让用户选择一个生成 Setup 的配置
        '''
        # 读取现在的所有 setup 配置
        setupCfg = self.leeConfig.get('SetupConfigure')

        # 若只有一个配置, 则直接选中这个配置, 进入用户确认阶段
        if len(setupCfg) > 1:
            # 列出来让用户进行选择, 选中哪个就把配置读取出来返回
            menus = []
            for cfgKey, cfgValue in enumerate(setupCfg):
                menuItem = [cfgValue['LeeName'], None, cfgKey]
                menus.append(menuItem)

            configure = self.leeCommon.menu(items=menus,
                                            title='选择生成配置',
                                            prompt='请选择用于生成安装程序的配置',
                                            inject=self,
                                            cancelExec='inject.choiceExit()',
                                            withCancel=True,
                                            resultMap=setupCfg)
        else:
            configure = setupCfg[0]

        # 把配置内容列出来让用户进行最终确认
        lines = self.__getConfigureInfos(configure)
        title = '确认配置的各个选项值'
        prompt = '是否继续?'

        if not self.leeCommon.confirm(lines, title, prompt, None, None, None):
            self.leeCommon.exitWithMessage('感谢您的使用, 再见')

        return configure

    def __getConfigureInfos(self, configure, dontPrint=True):
        '''
        给定一个配置的字典对象, 把内容构建成可读样式, 必要的话打印出来
        '''
        configureInfos = [
            '配置名称(LeeName): %s' % configure['LeeName'],
            '安装包唯一编号(LeeAppId): %s' % configure['LeeAppId'],
            '游戏名称(LeeAppName): %s' % configure['LeeAppName'],
            '游戏主程序(LeeAppExeName): %s' % configure['LeeAppExeName'],
            '安装包版本号(LeeAppVersion): %s' % configure['LeeAppVersion'],
            '安装包发布者(LeeAppPublisher): %s' % configure['LeeAppPublisher'],
            '发布者官网(LeeAppURL): %s' % configure['LeeAppURL'],
            '开始菜单程序组名称(LeeDefaultGroupName): %s' %
            configure['LeeDefaultGroupName'],
            '设置程序在开始菜单的名称(LeeGameSetupName): %s' %
            configure['LeeGameSetupName'],
            '设置程序在安装目录中的实际文件名(LeeGameSetupExeName): %s' %
            configure['LeeGameSetupExeName'],
            '安装时的默认目录名(LeeDefaultDirName): %s' %
            configure['LeeDefaultDirName'],
            '最终输出的安装程序文件名(LeeOutputBaseFilename): %s' %
            configure['LeeOutputBaseFilename'],
            '----------------------------------------------------------------',
            '若想修改以上选项的值, 或者想对他们的作用有更详细的了解, 请编辑',
            'Utility 目录下的 LeeClientAgent.yml 配置文件.',
            '----------------------------------------------------------------'
        ]

        if not dontPrint:
            print('\r\n'.join(configureInfos))
        return configureInfos

    def __readScriptTemplate(self):
        '''
        获取 Inno Setup 的脚本模板并作为字符串返回
        '''
        scriptDir = self.leeCommon.utility(withmark=False)
        scriptTemplateFilepath = (
            '%s/Bin/InnoSetup/Resources/Scripts/Scripts_Template.iss' %
            scriptDir).replace('/', os.path.sep)

        if not self.leeCommon.isFileExists(scriptTemplateFilepath):
            return None
        return open(scriptTemplateFilepath, 'r', encoding='utf-8').read()

    def __generateFinallyScript(self, templateContent, configure):
        '''
        把配置套到模板中, 并将处理后的脚本内容返回
        '''
        finallyContent = templateContent

        for k, v in configure.items():
            if v is None:
                continue
            rePattern = '(#define %s ".*?")' % k
            searchResult = re.search(rePattern, finallyContent)
            if searchResult is None:
                continue
            finallyContent = finallyContent.replace(searchResult.group(0),
                                                    '#define %s "%s"' % (k, v))

        return finallyContent

    def __saveFinallyScriptToCache(self, finallyContent):
        '''
        将给定的最终脚本内容保存到一个临时目录中, 并返回脚本的全路径
        '''
        scriptDir = self.leeCommon.utility(withmark=False)
        scriptCacheDir = ('%s/Bin/InnoSetup/Cache/' % scriptDir).replace(
            '/', os.path.sep)
        os.makedirs(scriptCacheDir, exist_ok=True)

        contentHash = self.leeCommon.getMD5ForString(finallyContent)
        scriptCachePath = os.path.abspath('%s%s.iss' %
                                          (scriptCacheDir, contentHash))

        if self.leeCommon.isFileExists(scriptCachePath):
            os.remove(scriptCachePath)

        # 这里保存脚本文件的时候必须用 UTF8 with BOM (对应的 encoding 为 utf-8-sig)
        # 否则 Inno Setup 引擎将无法识别出这是 UTF8 编码, 从而改用 ANSI 去解读脚本文件
        # 最后导致的结果就是中文快捷方式的名称直接变成了乱码
        cfile = open(scriptCachePath, 'w+', encoding='utf-8-sig')
        cfile.write(finallyContent)
        cfile.close()

        return scriptCachePath

    def __runInnoScript(self, scriptPath):
        '''
        调用 ISCC.exe 进行安装程序的制作, 同步等待结束
        '''
        innoSetupDir = self.__getInnoSetupInstallPath()
        if innoSetupDir is None:
            return False

        isccPath = '%sISCC.exe' % innoSetupDir
        if not self.leeCommon.isFileExists(isccPath):
            return False

        isccProc = subprocess.Popen('%s "%s"' % (isccPath, scriptPath),
                                    stdout=sys.stdout,
                                    cwd=os.path.dirname(scriptPath))
        isccProc.wait()

        return isccProc.returncode == 0