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
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()
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)
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
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 进行反馈.')
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