import os.path from pathlib import Path import re from xml.dom import minidom import javaproperties import requests from lxml import etree import xml.etree.ElementTree as ET from scripts.context import Context from scripts.task import Task from utils import FileUtils from utils.logger_utils import app_logger def process_manifest(input_path, output_path): # 解析XML文件 tree = ET.parse(input_path) root = tree.getroot() # 定义命名空间映射 android_namespace = 'http://schemas.android.com/apk/res/android' ET.register_namespace('android', android_namespace) namespaces = {'android': android_namespace} # 处理application标签,移除所有属性 application = root.find('application') if application is not None: # 保存所有子节点 children = list(application) # 清除application标签 root.remove(application) # 创建新的application标签(无属性) new_application = ET.Element('application') # 添加回所有子节点 for child in children: new_application.append(child) # 将新的application标签添加回root root.append(new_application) # 查找并删除指定的activity节点 activity_to_remove = new_application.find( ".//activity[@android:name='com.unity3d.player.UnityPlayerActivity']", namespaces=namespaces ) if activity_to_remove is not None: new_application.remove(activity_to_remove) # 保存处理后的XML,保留android命名空间前缀 rough_string = ET.tostring(root, 'utf-8') reparsed = minidom.parseString(rough_string) pretty_xml = reparsed.toprettyxml(indent=" ", encoding='utf-8') # 去除空行 lines = pretty_xml.decode('utf-8').split('\n') non_empty_lines = [line for line in lines if line.strip() != ''] pretty_xml = '\n'.join(non_empty_lines).encode('utf-8') with open(output_path, 'wb') as f: f.write(pretty_xml) print(f"处理完成,结果已保存到 {output_path}") def uncomment_line(content, line_pattern): """ 取消指定行的注释 :param content: 文件内容 :param line_pattern: 要取消注释的行内容(不含前导//和空格) :return: 更新后的内容 """ # 匹配以//开头,后跟任意空格,然后是目标行内容 pattern = rf'^(\s*)//\s*({re.escape(line_pattern)}\s*)$' # 替换为去注释版本(保留原有缩进) replacement = rf'\1\2' updated_content = re.sub(pattern, replacement, content, flags=re.MULTILINE) return updated_content def update_gradle_variable(content, variable_name, new_value): """ 更新 Gradle 文件中的 final def 变量值 :param content: Gradle 文件内容 :param variable_name: 变量名 (如 "releaseName") :param new_value: 新值 (字符串或数字) :return: 更新后的内容 """ # 处理字符串值(带引号) if isinstance(new_value, str): # 匹配带引号的字符串赋值 pattern = rf'(final\s+def\s+{re.escape(variable_name)}\s*=\s*["\'])(.*?)(["\'])' replacement = rf'\g<1>{new_value}\g<3>' else: # 匹配数字值(不带引号) pattern = rf'(final\s+def\s+{re.escape(variable_name)}\s*=\s*)(\d+)' replacement = rf'\g<1>{new_value}' updated_content = re.sub(pattern, replacement, content) return updated_content def modify_text_with_regex(original_text, key, new_value): """ 使用正则表达式修改文本中指定key的值,支持带引号和数字类型 参数: original_text -- 原始文本内容 key -- 要修改的键名 new_value -- 新的值 返回: 修改后的文本内容 """ # 改进的正则模式:更精确地匹配带引号的值和数字值 # 匹配带引号的值(单引号或双引号)或数字值 pattern = re.compile( r'(final def ' + re.escape(key) + r' = )' # 前缀部分 r'(?:' # 非捕获组,用于分组不同情况 r'([\'"])(.*?)\2' # 带引号的值(单引号或双引号) r'|' # 或者 r'(\d+)' # 数字值(整数) r')' ) # 查找匹配 match = pattern.search(original_text) if not match: print(f"未找到键: {key}") return original_text # 检查是带引号的情况还是数字情况 quote_type = match.group(2) # 引号类型(单引号或双引号) is_number = match.group(4) is not None # 是否是数字类型 # 构造替换字符串 if quote_type: # 有引号的情况,保持原有引号类型 replacement = f'\g<1>{quote_type}{new_value}{quote_type}' elif is_number: # 数字类型,直接替换数字 replacement = f'\g<1>{new_value}' else: # 其他情况,保持原样替换 replacement = f'\g<1>{new_value}' # 执行替换,只替换第一个匹配项 modified_text = pattern.sub(replacement, original_text, count=1) return modified_text def update_gradle_property(content, key, new_value): # 匹配两种格式: # 1. resValue "string", "key", "value" # 2. resValue("string", "key", "value") pattern = rf'(resValue\s*\(?\s*["\']string["\']\s*,\s*["\']{re.escape(key)}["\']\s*,\s*["\'])(.*?)(["\']\s*\)?)' # 替换为新值 updated_content = re.sub(pattern, rf'\g<1>{new_value}\g<3>', content) return updated_content LAUNCHER_CODE_PATH = f"LauncherCode/src/com/launchercode".replace("/", os.sep) GAME_ACTIVITY_PATH = f"launcher-game/src/com/game/launcher/activity/GLGameActivity.kt".replace("/", os.sep) GAME_ACTIVITY_PATH_2 = f"LauncherCode/src/com/launchercode/activity/GameActivity.kt".replace("/", os.sep) ANDROID_MANIFEST_PATH = f"launcher-game/AndroidManifest.xml".replace("/", os.sep) STRING_PATH = f"launcher-game/res/values/strings.xml".replace("/", os.sep) LAUNCER_STRING_PATH = f"LauncherCode/src/com/launchercode/LauncherStringsValue.kt".replace("/", os.sep) class ProjectUpdate(Task): def __init__(self, context: Context): super().__init__(context) self.build_gradle_path = None def update_package_name(self): """ 更新包名 :return: """ build_gradle_path = os.path.join(self.context.temp_project_path, "ad.gradle") text = open(build_gradle_path, "r", encoding="utf-8").read() text = text.replace(self.context.original_package, self.context.package_name) open(build_gradle_path, "w", encoding="utf-8").write(text) xml_path = os.path.join(self.context.temp_project_path, "lawnchair/res/xml") for root, dirs, files in os.walk(xml_path): for file in files: temp_xml_path = os.path.join(root, file) text = open(temp_xml_path, "r", encoding="utf-8").read() text = text.replace(self.context.original_package, self.context.package_name) open(temp_xml_path, "w", encoding="utf-8").write(text) pass pass def update_keystore(self): root_dir = os.path.join("game_config", self.context.package_name) for file in os.listdir(root_dir): if file.endswith(".keystore"): name = file.replace(".keystore", "") FileUtils.copy(os.path.join(root_dir, file), os.path.join(self.context.temp_project_path, file)) open(os.path.join(self.context.temp_project_path, "keystore.properties"), "w", encoding="utf-8").write( f""" keyAlias={name} keyPassword=123456 storeFile=./{name}.keystore storePassword=123456 """ ) return raise Exception("keystore not found") def update_config(self): """ 更新配置文件 :return: """ root_dir = os.path.join("game_config", self.context.package_name) config_path = list( filter(lambda f: f.endswith(".zip") and f.startswith(self.context.package_name), os.listdir(root_dir))) if len(config_path) <= 0: raise Exception("config not found") target_path = os.path.join(root_dir, config_path[0]) dst = os.path.join(self.context.temp_project_path, config_path[0].replace(".zip", "")) result = FileUtils.decompress(target_path, dst) app_logger().debug(f"{target_path} -> {dst} , 解压结果: {result}") mainly_path = os.path.join(dst, "mainly") if not os.path.exists(mainly_path): mainly_path = os.path.join(dst, "appConfig") google_services_json_path = os.path.join(dst, "google-services.json") if not os.path.exists(google_services_json_path): google_services_json_path = os.path.join(dst, "appConfig", "google-services.json") FileUtils.copy(google_services_json_path, os.path.join(self.context.temp_project_path, "google-services.json"), True) dst_path = os.path.join(self.context.temp_project_path, f"launcher-game{os.sep}assets") for file in list(filter(lambda f: not (f == "google_fonts.json" or f == "pag_gl_slide.pag"), os.listdir(dst_path))): FileUtils.delete(os.path.join(dst_path, file), True) pass for file in list(filter(lambda f: f.find(".") <= 0, os.listdir(mainly_path))): FileUtils.copy(os.path.join(mainly_path, file), os.path.join(dst_path, file)) with open(os.path.join(dst, "tkg_config_mainly.properties"), 'rb') as f: self.context.config = javaproperties.load(f) # 不打admob if self.context.admob_app_id is None or self.context.admob_app_id == "": self.context.admob_app_id = self.context.get_config("admob_id") pass def update_icon(self): """ 更新游戏Icon :return: """ target_icon_path = os.path.join("game_config", self.context.package_name, "icon.zip") tag = "res_icon_resources" dst = os.path.join(self.context.temp_project_path, tag) FileUtils.decompress(target_icon_path, dst) for root, dirs, files in os.walk(dst): for file in files: temp_tart_path = os.path.join(root, file) if temp_tart_path.find("__MACOSX") > 0: continue temp_dst = temp_tart_path.replace(tag, "launcher-game" + os.sep + "res") app_logger().debug(f"copy icon = {temp_tart_path} -> {temp_dst}") FileUtils.copy(temp_tart_path, temp_dst, True) pass def update_game_result(self): """ 更新游戏资源 :return: """ root_dir = os.path.join("game_config", self.context.package_name) if self.context.game_type == "unity_native": res_path = os.path.join(root_dir, "unityLibrary.zip") if not os.path.exists(res_path): raise Exception("unity library not found") dst = os.path.join(self.context.temp_project_path, "unityLibrary") temp_dst = dst + "_res" if os.path.exists(dst): FileUtils.delete(dst, True) if os.path.exists(temp_dst): FileUtils.delete(temp_dst, True) FileUtils.decompress(res_path, temp_dst) build_path = os.path.join(temp_dst, "build") if os.path.exists(build_path): FileUtils.delete(build_path, True) if os.listdir(temp_dst).index("unityLibrary") >= 0: FileUtils.copy(os.path.join(temp_dst, "unityLibrary"), dst) else: FileUtils.copy(temp_dst, dst) android_manifest_xml_path = os.path.join(dst, "src", "main", "AndroidManifest.xml") process_manifest(android_manifest_xml_path, android_manifest_xml_path) text = open(os.path.join(dst, "build.gradle"), "r", encoding="utf-8").read() text = text.replace("implementation", "api") if not 'namespace "com.unity3d.player"' in text: text = text.replace("compileSdkVersion", """ namespace "com.unity3d.player" compileSdkVersion""") text = text.replace("unityStreamingAssets.tokenize(', ')", '[".unity3d", ".bundle", ".version", ".bytes", ".hash"]') text = text.replace("apply plugin: 'com.android.library'", """ plugins { id 'com.android.library' } """) open(os.path.join(dst, "build.gradle"), "w", encoding="utf-8").write(text) lines = open(os.path.join(dst, "build.gradle"), "r", encoding="utf-8").readlines() new_lines = [] for line in lines: if line.find("com.game:hachisdk") > 0: continue new_lines.append(line) open(os.path.join(dst, "build.gradle"), "w", encoding="utf-8").writelines(new_lines) # 引用Unity项目 text = open(os.path.join(self.context.temp_project_path, "ad.gradle"), "r", encoding="utf-8").read() text = uncomment_line(text, "implementation projects.unityLibrary") open(os.path.join(self.context.temp_project_path, "ad.gradle"), "w", encoding="utf-8").write(text) text = open(os.path.join(self.context.temp_project_path, "settings.gradle"), "r", encoding="utf-8").read() text = uncomment_line(text, "include ':unityLibrary'") open(os.path.join(self.context.temp_project_path, "settings.gradle"), "w", encoding="utf-8").write(text) # launcher 引用 unityActivity text = open(os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH), "r", encoding="utf-8").read() text = text.replace("GLGameWebActivity", "com.unity3d.player.UnityPlayerActivity") open(os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH), "w", encoding="utf-8").write(text) text = open(os.path.join(self.context.temp_project_path, ANDROID_MANIFEST_PATH), "r", encoding="utf-8").read() text = text.replace("@style/LauncherGameIntroTheme", "@style/UnityThemeSelector") open(os.path.join(self.context.temp_project_path, ANDROID_MANIFEST_PATH), "w", encoding="utf-8").write(text) else: raise Exception(f"不支持的游戏类型 : {self.context.game_type}") pass def update_image(self): """ 更新游戏的资源 :return: """ root_dir = os.path.join("game_config", self.context.package_name) drawable_path = os.path.join(root_dir, "drawable-xxhdpi.zip") if not os.path.exists(drawable_path): raise Exception("drawable not found") dst = os.path.join(self.context.temp_project_path, "drawable_res") FileUtils.decompress(drawable_path, dst) if os.path.join(dst, "drawable-xxhdpi"): dst = os.path.join(dst, "drawable-xxhdpi") target_root_path = os.path.join(self.context.temp_project_path, f"launcher-game{os.sep}res{os.sep}drawable-xxhdpi") image_list = list(map(lambda f: Path(f).stem, os.listdir(target_root_path))) for file in os.listdir(dst): if file == ".DS_Store": continue temp_tar = os.path.join(dst, file) temp_dst = os.path.join(target_root_path, file) file_name = Path(file).stem if file_name in image_list: FileUtils.delete(os.path.join(target_root_path, file_name + ".png")) FileUtils.delete(os.path.join(target_root_path, file_name + ".jpg")) pass FileUtils.copy(temp_tar, temp_dst, True) pass def update_gradle_config(self): """ 更新gradle里面的版本号 :return: """ build_gradle_path = os.path.join(self.context.temp_project_path, "ad.gradle") text = open(build_gradle_path, "r", encoding="UTF-8").read() text = modify_text_with_regex(text, "admob_app_id", self.context.admob_app_id) text = modify_text_with_regex(text, "game_services_project_id", self.context.game_services_project_id) text = modify_text_with_regex(text, "facebook_app_id", self.context.facebook_app_id) text = modify_text_with_regex(text, "facebook_client_token", self.context.facebook_client_token) text = modify_text_with_regex(text, "appName", self.context.get_app_name()) text = modify_text_with_regex(text, "appVersionName", self.context.version_display_name) text = modify_text_with_regex(text, "appVersionCode", self.context.version_code) open(build_gradle_path, "w", encoding="UTF-8").write(text) pass def update_string(self): privacy = self.context.get_config("TkA_Url_Privacy") if not privacy or privacy == "": raise Exception("配置文件中没有配置 TkA_Url_Privacy") tkg_custom = self.context.get_config("tkg_custom") if not tkg_custom or tkg_custom == "": raise Exception("配置文件中没有配置 tkg_custom") text = open(os.path.join(self.context.temp_project_path, STRING_PATH), "r", encoding="utf-8").read() text = text.replace("https://doanvanquy.com/privacy.html", privacy) text = text.replace("https://doanvanquy.com/TermsOfUse.html", privacy.replace("privacy.html", "TermsOfUse.html")) open(os.path.join(self.context.temp_project_path, STRING_PATH), "w", encoding="utf-8").write(text) # text = open(os.path.join(self.context.temp_project_path, LAUNCER_STRING_PATH), "r", encoding="utf-8").read() # text = text.replace("https://harmonitun.com/privacy.html", privacy) # text = text.replace("https://harmonitun.com/TermsOfUse.html", # privacy.replace("privacy.html", "TermsOfUse.html")) # text = text.replace("harmounitun@outlook.com", tkg_custom) # open(os.path.join(self.context.temp_project_path, LAUNCER_STRING_PATH), "w", encoding="utf-8").write(text) pass def execute(self): global GAME_ACTIVITY_PATH path = os.path.join(self.context.temp_project_path, GAME_ACTIVITY_PATH) if not os.path.exists(path): GAME_ACTIVITY_PATH = GAME_ACTIVITY_PATH_2 # GAME_ACTIVITY_PATH = GAME_ACTIVITY_PATH_2 pass self.build_gradle_path = os.path.join(self.context.temp_project_path, "build.gradle") self.update_package_name() self.update_keystore() self.update_config() self.update_icon() self.update_image() self.update_game_result() self.update_gradle_config() self.update_string() pass