Example #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
Example #2
0
class LeeBaseRevert:
    def __init__(self):
        self.leeCommon = LeeCommon()
        self.revertDefaultDBPath = None
        self.revertFiles = []

    def clearRevert(self):
        self.revertFiles.clear()

    def loadRevert(self, revertDatabaseLoadpath=None):
        if revertDatabaseLoadpath is None:
            revertDatabaseLoadpath = self.revertDefaultDBPath
        revertDatabaseLoadpath = '%s/%s' % (self.leeCommon.utility(
            withmark=False), revertDatabaseLoadpath)
        if not self.leeCommon.isFileExists(revertDatabaseLoadpath):
            return False

        self.revertFiles.clear()
        revertContent = json.load(
            open(revertDatabaseLoadpath, 'r', encoding='utf-8'))
        self.revertFiles = revertContent['files']

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

    def rememberRevert(self, filepath):
        patchesDir = self.leeCommon.patches()
        self.revertFiles.append(
            os.path.relpath(filepath, patchesDir).replace('/', '\\'))

    def saveRevert(self, revertDatabaseSavepath=None):
        if revertDatabaseSavepath is None:
            revertDatabaseSavepath = self.revertDefaultDBPath
        revertDatabaseSavepath = '%s/%s' % (self.leeCommon.utility(
            withmark=False), revertDatabaseSavepath)
        if self.leeCommon.isFileExists(revertDatabaseSavepath):
            os.remove(revertDatabaseSavepath)
        os.makedirs(os.path.dirname(revertDatabaseSavepath), exist_ok=True)

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

        json.dump({'files': self.revertFiles},
                  open(revertDatabaseSavepath, 'w', encoding='utf-8'),
                  indent=4,
                  ensure_ascii=False)

    def canRevert(self, revertDatabaseLoadpath=None):
        if revertDatabaseLoadpath is None:
            revertDatabaseLoadpath = self.revertDefaultDBPath
        self.loadRevert(revertDatabaseLoadpath)
        return len(self.revertFiles) > 0

    def doRevert(self, revertDatabaseLoadpath=None):
        if revertDatabaseLoadpath is None:
            revertDatabaseLoadpath = self.revertDefaultDBPath
        self.loadRevert(revertDatabaseLoadpath)

        patchesDir = self.leeCommon.patches()

        for relpath in self.revertFiles:
            # replace('\\', os.path.sep) 标准化处理一下反斜杠
            fullpath = os.path.abspath(
                '%s/%s' % (patchesDir, relpath.replace('\\', os.path.sep)))
            if self.leeCommon.isFileExists(fullpath):
                os.remove(fullpath)

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

        self.leeCommon.removeEmptyDirectorys(patchesDir)
Example #3
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()
Example #4
0
class LeeLua:
    def __init__(self):
        self.leeCommon = LeeCommon()
        self.sourceDirectory = None
        self.outputDirectory = None
        self.grfCLFilepath = None
        self.baseDirectory = None

    def __getOutputFilePath(self, sourcePath):
        relpath = os.path.relpath(sourcePath, self.sourceDirectory)
        outputpath = os.path.abspath(
            '%s%s%s' % (self.outputDirectory, os.path.sep, relpath))
        return outputpath

    def isTrulyLubFile(self, filepath):
        rawfile = open(filepath, 'rb')
        magicHeader = rawfile.read(6)
        rawfile.close()

        return (magicHeader[0] == 0x1B and magicHeader[1] == 0x4C
                and magicHeader[2] == 0x75 and magicHeader[3] == 0x61
                and magicHeader[4] == 0x51 and magicHeader[5] == 0x00)

    def __replaceFunctionStruct(self, line):
        matches = re.match(r'^(\w*?)\s=\sfunction\((.*?)\)(.*?)$', line)
        if not matches:
            return line
        return 'function {funname}({params})'.format(funname=matches.group(1),
                                                     params=matches.group(2))

    def __removeFunctionNote(self, line):
        if line.startswith('-- Function #'):
            return None
        return line

    def __lubAmendmentsByFile(self, filename, content):

        # 移除 GRF Editor Decompiler 标记
        markRemoveIndex = []
        for index, line in enumerate(content):
            if re.match('^-- Using GRF Editor Decompiler.*?$', line):
                content[index] = None
                markRemoveIndex.append(index + 1)
            if index in markRemoveIndex:
                content[index] = None

        # 若文件的末尾没有空行的话, 补加一个空行
        if not content:
            content.append('')
        if str(content[-1]).strip() != '' and not str(
                content[-1]).endswith('\n'):
            content.append('')

        # if filename.lower() == 'kaframovemapservicelist.lub':
        #     pass

        content = [x for x in content if x is not None]
        return content

    def lubAmendments(self, srcfilepath, dstfilepath):
        encoding = self.leeCommon.getEncodingByFile(srcfilepath)
        encoding = 'latin1' if encoding is None else encoding

        try:
            luafile = open(srcfilepath, 'r', encoding=encoding, newline='')
            content = luafile.readlines()
            luafile.close()

            # 按行进行处理
            content = [
                x.replace('\r\n', '\n').replace('\n', '') for x in content
            ]
            for index, line in enumerate(content):
                line = self.__replaceFunctionStruct(line)
                line = self.__removeFunctionNote(line)
                content[index] = line
            content = [x for x in content if x is not None]

            # 按文件进行处理
            content = self.__lubAmendmentsByFile(os.path.basename(srcfilepath),
                                                 content)

            savefile = open(dstfilepath, 'w', encoding=encoding, newline='')
            savefile.write('\r\n'.join(content))
            savefile.close()
            return True
        except Exception as _err:
            print('对 lub 文件进行处理时发生错误: %s' % srcfilepath)
            raise

    def amendmentsDir(self, lubSourceDirectory, lubOutputDirectory):
        self.sourceDirectory = lubSourceDirectory
        self.outputDirectory = lubOutputDirectory
        self.baseDirectory = os.path.dirname(self.sourceDirectory)

        for dirpath, _dirnames, filenames in os.walk(lubSourceDirectory):
            for filename in filenames:
                fullpath = os.path.normpath('%s/%s' % (dirpath, filename))
                destpath = self.__getOutputFilePath(fullpath)
                os.makedirs(os.path.dirname(destpath), exist_ok=True)

                if fullpath.lower().endswith(
                        '.lub') and not self.isTrulyLubFile(fullpath):
                    self.lubAmendments(fullpath, destpath)
                    print('整理完毕: ' +
                          os.path.relpath(fullpath, self.baseDirectory))
                else:
                    shutil.copyfile(fullpath, destpath)
                    print('已经复制: ' +
                          os.path.relpath(fullpath, self.baseDirectory))

    def getLubEncoding(self, filepath):
        if not self.isTrulyLubFile(filepath):
            return True, self.leeCommon.getEncodingByFile(filepath)
        else:
            return False, None

    def decodeFile(self, lubSourcePath, lubOutputPath):
        grfCLProc = subprocess.Popen(
            '%s %s' %
            (self.grfCLFilepath, '-breakOnExceptions true -lub "%s" "%s"' %
             (lubSourcePath, lubOutputPath)),
            stdout=subprocess.PIPE,
            cwd=os.path.dirname(self.grfCLFilepath))
        grfCLProc.wait()

        # 确认结果并输出提示信息表示反编译结束
        if grfCLProc.returncode == 0 and self.leeCommon.isFileExists(
                lubOutputPath):
            print('已输出到: ' +
                  os.path.relpath(lubOutputPath, self.baseDirectory))
            self.lubAmendments(lubOutputPath, lubOutputPath)
            return True

        print('进行反编译时发生错误: ' +
              os.path.relpath(lubSourcePath, self.baseDirectory))
        return False

    def decodeDir(self, lubSourceDirectory, lubOutputDirectory):
        # 记录到成员变量里面
        self.sourceDirectory = lubSourceDirectory
        self.outputDirectory = lubOutputDirectory
        self.baseDirectory = os.path.dirname(self.sourceDirectory)

        # 确认操作系统平台
        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'
            )

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

        for dirpath, _dirnames, filenames in os.walk(lubSourceDirectory):
            for filename in filenames:
                fullpath = os.path.normpath('%s/%s' % (dirpath, filename))
                destpath = self.__getOutputFilePath(fullpath)
                os.makedirs(os.path.dirname(destpath), exist_ok=True)

                print('')
                if fullpath.lower().endswith('.lub') and self.isTrulyLubFile(
                        fullpath):
                    print('需反编译: ' +
                          os.path.relpath(fullpath, self.baseDirectory))
                    if not self.decodeFile(fullpath, destpath):
                        print('失败复制: ' +
                              os.path.relpath(fullpath, self.baseDirectory))
                        shutil.copyfile(fullpath, destpath)
                else:
                    print('直接复制: ' +
                          os.path.relpath(fullpath, self.baseDirectory))
                    shutil.copyfile(fullpath, destpath)
Example #5
0
class LeeButtonRender:
    def __init__(self):
        self.leeCommon = LeeCommon()
        self.btnConfigure = {}
        self.fontPathMap = {}
        pygame.font.init()

    def autoCrop(self, image, backgroundColor=None):
        '''Intelligent automatic image cropping.
            This functions removes the usless "white" space around an image.
            If the image has an alpha (tranparency) channel, it will be used
            to choose what to crop.
            Otherwise, this function will try to find the most popular color
            on the edges of the image and consider this color "whitespace".
            (You can override this color with the backgroundColor parameter)
            Input:
                image (a PIL Image object): The image to crop.
                backgroundColor (3 integers tuple): eg. (0,0,255)
                    The color to consider "background to crop".
                    If the image is transparent, this parameters will be ignored.
                    If the image is not transparent and this parameter is not
                    provided, it will be automatically calculated.
            Output:
                a PIL Image object : The cropped image.
        '''
        def mostPopularEdgeColor(image):
            ''' Compute who's the most popular color on the edges of an image.
                (left,right,top,bottom)
                Input:
                    image: a PIL Image object
                Ouput:
                    The most popular color (A tuple of integers (R,G,B))
            '''
            im = image
            if im.mode != 'RGB':
                im = image.convert('RGB')
            # Get pixels from the edges of the image:
            width, height = im.size
            left = im.crop((0, 1, 1, height - 1))
            right = im.crop((width - 1, 1, width, height - 1))
            top = im.crop((0, 0, width, 1))
            bottom = im.crop((0, height - 1, width, height))
            pixels = left.tostring() + right.tostring() + top.tostring(
            ) + bottom.tostring()
            # Compute who's the most popular RGB triplet
            counts = {}
            for i in range(0, len(pixels), 3):
                RGB = pixels[i] + pixels[i + 1] + pixels[i + 2]
                if RGB in counts:
                    counts[RGB] += 1
                else:
                    counts[RGB] = 1
            # Get the colour which is the most popular:
            mostPopularColor = sorted([(count, rgba)
                                       for (rgba, count) in counts.items()],
                                      reverse=True)[0][1]
            return ord(mostPopularColor[0]), ord(mostPopularColor[1]), ord(
                mostPopularColor[2])

        bbox = None
        # If the image has an alpha (tranparency) layer, we use it to crop the image.
        # Otherwise, we look at the pixels around the image (top, left, bottom and right)
        # and use the most used color as the color to crop.
        # --- For transparent images -----------------------------------------------
        if 'A' in image.getbands(
        ):  # If the image has a transparency layer, use it.
            # This works for all modes which have transparency layer
            bbox = image.split()[list(image.getbands()).index('A')].getbbox()
        # --- For non-transparent images -------------------------------------------
        elif image.mode == 'RGB':
            if not backgroundColor:
                backgroundColor = mostPopularEdgeColor(image)
            # Crop a non-transparent image.
            # .getbbox() always crops the black color.
            # So we need to substract the "background" color from our image.
            bg = Image.new('RGB', image.size, backgroundColor)
            diff = ImageChops.difference(
                image, bg)  # Substract background color from image
            bbox = diff.getbbox(
            )  # Try to find the real bounding box of the image.
        else:
            raise NotImplementedError(
                "Sorry, this function is not implemented yet for images in mode '%s'."
                % image.mode)
        if bbox:
            image = image.crop(bbox)
        return image

    def createButtonImage(self, tplName, btnState, btnText, btnWidth):
        self.btnConfigure = self.__loadButtonConfigure(tplName)
        globalOffsetX = int(
            self.__getButtonConfigureValue(btnState,
                                           'globalOffset').split(',')[0])
        globalOffsetY = int(
            self.__getButtonConfigureValue(btnState,
                                           'globalOffset').split(',')[1])

        imgButton = self.createButtonBackgroundImage(tplName, btnState,
                                                     btnWidth)
        imgText = self.createTextImage(btnText, btnState)

        imgButtonWidth, imgButtonHeight = imgButton.size
        imgTextWidth, imgTextHeight = imgText.size

        pasteRect = (((imgButtonWidth - imgTextWidth) // 2) + globalOffsetX,
                     ((imgButtonHeight - imgTextHeight) // 2) + globalOffsetY,
                     (((imgButtonWidth - imgTextWidth) // 2) + imgTextWidth) +
                     globalOffsetX, (((imgButtonHeight - imgTextHeight) // 2) +
                                     imgTextHeight) + globalOffsetY)

        imgButton = imgButton.convert('RGB')
        imgButton.paste(imgText, pasteRect, mask=imgText)

        return imgButton

    def createButtonBmpFile(self, tplName, btnState, btnText, btnWidth,
                            savePath):
        btnImage = self.createButtonImage(tplName, btnState, btnText, btnWidth)
        btnImage.save(os.path.abspath(savePath), 'bmp')
        return True

    def createButtonBackgroundImage(self, tplName, btnState, btnWidth):
        pathLeftPiece = self.getButtonTemplatePath(tplName, btnState, 'left')
        pathMidPiece = self.getButtonTemplatePath(tplName, btnState, 'mid')
        pathRightPiece = self.getButtonTemplatePath(tplName, btnState, 'right')

        imgLeftPiece = Image.open(pathLeftPiece)
        imgMidPiece = Image.open(pathMidPiece)
        imgRightPiece = Image.open(pathRightPiece)

        imgLeftWidth, imgLeftHeight = imgLeftPiece.size
        imgMidWidth, imgMidHeight = imgMidPiece.size
        imgRightWidth, imgRightHeight = imgRightPiece.size

        if not imgLeftHeight == imgMidHeight == imgRightHeight:
            print('左中右三张切图文件的高度必须完全匹配')

        # 解析来开始拼接按钮的背景图片
        # 建立一个 btnWidth x imgLeftHeight 的图片对象, 底色用 RO 的透明色 FF40FF
        imgButton = Image.new('RGBA', (btnWidth, imgLeftHeight), '#FF40FF')

        # 将中间的图片填满除了左右两侧之外的中央的区域
        midSpace = btnWidth - imgLeftWidth - imgRightWidth
        repeatTime = 0
        while midSpace > 0:
            left = imgLeftWidth + (imgMidWidth * repeatTime)
            imgButton.paste(imgMidPiece,
                            (left, 0, left + imgMidWidth, imgMidHeight))
            repeatTime = repeatTime + 1
            midSpace = midSpace - imgMidWidth

        # 将左侧的图片填充到按钮背景的最左侧
        imgButton.paste(imgLeftPiece, (0, 0, imgLeftWidth, imgLeftHeight))

        # 将右侧的图片填充到按钮背景的最右侧
        imgButton.paste(
            imgRightPiece,
            (btnWidth - imgRightWidth, 0, btnWidth, imgRightHeight))

        # 尝试进行一些资源文件的释放, 但这里不会去释放 imgButton
        imgLeftPiece.close()
        imgMidPiece.close()
        imgRightPiece.close()

        return imgButton

    def createTextImage(self, btnText, btnState):
        fontName, fontSize = self.__getButtonFontInfomation()
        fontPath = self.getFontPath(fontName)
        foreFontColor = self.leeCommon.strHexToRgb(
            self.__getButtonConfigureValue(btnState, 'fontColor'))
        shadowFontColor = self.leeCommon.strHexToRgb(
            self.__getButtonConfigureValue(btnState, 'shadowColor'))
        shadowFontAlpha = int(
            self.__getButtonConfigureValue(btnState, 'shadowAlpha'))

        # 根据不同的阴影类型, 进行渲染
        if self.__getButtonConfigureValue(btnState, 'shadowMode') == 'offset':

            # 进行前端文字的渲染
            pygameForeFont = pygame.font.Font(fontPath, fontSize)
            pygameForeText = pygameForeFont.render(btnText, True,
                                                   foreFontColor)
            pyForeTextStor = pygame.image.tostring(pygameForeText, 'RGBA',
                                                   False)
            imgForeText = Image.frombytes('RGBA', pygameForeText.get_size(),
                                          pyForeTextStor)
            imgForeText = self.autoCrop(imgForeText)

            # 进行阴影字体的渲染
            pygameBackFont = pygame.font.Font(fontPath, fontSize)
            pygameBackText = pygameBackFont.render(btnText, True,
                                                   shadowFontColor)
            pyBackTextStor = pygame.image.tostring(pygameBackText, 'RGBA',
                                                   False)
            imgBackText = Image.frombytes('RGBA', pygameBackText.get_size(),
                                          pyBackTextStor)
            imgBackText = self.autoCrop(imgBackText)

            # 对阴影字体应用指定透明度
            _red, _green, _blue, alpha = imgBackText.split()
            alpha = alpha.point(lambda i: i > 0 and
                                (255 / 100) * shadowFontAlpha)
            imgBackText.putalpha(alpha)

            # 文字的阴影的偏移叠加处理过程
            shadowOffsetX = int(
                self.__getButtonConfigureValue(btnState,
                                               'shadowOffset').split(',')[0])
            shadowOffsetY = int(
                self.__getButtonConfigureValue(btnState,
                                               'shadowOffset').split(',')[1])
            boardWidth = imgForeText.size[0] + abs(shadowOffsetX)
            boardHeight = imgForeText.size[1] + abs(shadowOffsetY)
            foreOffsetX = 0 if self.leeCommon.isPositive(
                shadowOffsetX) else abs(shadowOffsetX)
            foreOffsetY = 0 if self.leeCommon.isPositive(
                shadowOffsetY) else abs(shadowOffsetY)
            shadowOffsetX = 0 if not self.leeCommon.isPositive(
                shadowOffsetX) else shadowOffsetX
            shadowOffsetY = 0 if not self.leeCommon.isPositive(
                shadowOffsetY) else shadowOffsetY

            imgMergeText = Image.new('RGBA', (boardWidth, boardHeight),
                                     (0, 0, 0, 0))
            imgMergeText.paste(imgBackText, (shadowOffsetX, shadowOffsetY))
            imgMergeText.paste(imgForeText, (foreOffsetX, foreOffsetY),
                               mask=imgForeText)

            return imgMergeText

        elif self.__getButtonConfigureValue(btnState,
                                            'shadowMode') == 'outline':

            outFont = pygame.font.Font(fontPath, fontSize + 2)
            imgSurface = pygame.Surface(outFont.size(btnText), pygame.SRCALPHA)
            innerFont = pygame.font.Font(fontPath, fontSize)
            outline = innerFont.render(btnText, 1, shadowFontColor)

            w, h = imgSurface.get_size()
            ww, hh = outline.get_size()
            cx = w / 2 - ww / 2
            cy = h / 2 - hh / 2

            for x in range(-1, 2):
                for y in range(-1, 2):
                    imgSurface.blit(outline, (x + cx, y + cy))

            imgSurface.blit(innerFont.render(btnText, 1, foreFontColor),
                            (cx, cy))
            imgSurfaceTextStor = pygame.image.tostring(imgSurface, 'RGBA',
                                                       False)
            imgFinalText = Image.frombytes('RGBA', imgSurface.get_size(),
                                           imgSurfaceTextStor)
            imgFinalText = self.autoCrop(imgFinalText)

            return imgFinalText

    def getButtonTemplatePath(self, tplName, btnState, piece):
        return os.path.abspath(
            '%s/Resources/Texture/Button/Style_%s/%s_%s.png' %
            (self.leeCommon.utility(withmark=False), tplName, btnState, piece))

    def getFontPath(self, fontFilename):
        if fontFilename in self.fontPathMap:
            return self.fontPathMap[fontFilename]

        fontOriginPath = os.path.abspath(
            '%s/Resources/Fonts/%s' %
            (self.leeCommon.utility(withmark=False), fontFilename))

        # 把字体文件复制到系统临时目录, 以便确保路径没有任何中文
        fontTempPath = tempfile.mktemp(prefix='leefont_', suffix='.ttc')
        shutil.copyfile(fontOriginPath, fontTempPath)

        # 记住临时字体文件的路径避免重复复制
        self.fontPathMap[fontFilename] = fontTempPath
        return fontTempPath

    def getImageSizeByFilepath(self, filepath):
        img = Image.open(filepath)
        imgsize = img.size
        img.close()
        return imgsize

    def __loadButtonConfigure(self, tplName):
        configurePath = (
            '%s/Resources/Texture/Button/Style_%s/configure.json' %
            (self.leeCommon.utility(withmark=False), tplName))
        return (json.load(open(configurePath, 'r'))
                if self.leeCommon.isFileExists(configurePath) else None)

    def __getButtonFontInfomation(self):
        if self.btnConfigure:
            return self.btnConfigure['fontName'], int(
                self.btnConfigure['fontSize'])
        else:
            self.leeCommon.exitWithMessage(
                '__getButtonFontInfomation: 无法加载字体的配置信息')
            return None, None

    def __getButtonConfigureValue(self, btnState, attrib):
        if self.btnConfigure:
            return self.btnConfigure[btnState][attrib]
        else:
            self.leeCommon.exitWithMessage(
                '__getButtonConfigureValue: 无法加载字体的配置信息')
            return None, None
Example #6
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
Example #7
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 进行反馈.')
Example #8
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