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 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)
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 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)
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
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