diff --git a/build_config.json b/build_config.json index ddc95b0..e97b6e1 100644 --- a/build_config.json +++ b/build_config.json @@ -2,5 +2,8 @@ "repo_url": "http://192.168.0.200:3000/Faxing/Lawnchair.git", "repo_branch": "touka-dev", "repo_commit": "", - "package_name": "com.shape.shift.run.launcher.free.game.xjrtg" + "package_name": "com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw", + "game_type": "unity_native", + "version_display_name": "1.1.9", + "version_code": 19 } \ No newline at end of file diff --git a/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw_GooglePlay.zip b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw_GooglePlay.zip new file mode 100644 index 0000000..21ae675 Binary files /dev/null and b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw_GooglePlay.zip differ diff --git a/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/drawable-xxhdpi.zip b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/drawable-xxhdpi.zip new file mode 100644 index 0000000..4249e13 Binary files /dev/null and b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/drawable-xxhdpi.zip differ diff --git a/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/icon.zip b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/icon.zip new file mode 100644 index 0000000..fdb3353 Binary files /dev/null and b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/icon.zip differ diff --git a/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/punchprankster.keystore b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/punchprankster.keystore new file mode 100644 index 0000000..79b7a3b Binary files /dev/null and b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/punchprankster.keystore differ diff --git a/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/unityLibrary.zip b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/unityLibrary.zip new file mode 100644 index 0000000..0a1c25f Binary files /dev/null and b/game_config/com.punch.prankster.puzzle.fwaes.free.launcher.fgsaw/unityLibrary.zip differ diff --git a/game_config/com.shape.shift.run.launcher.free.game.xjrtg/com.shape.shift.run.launcher.free.game.xjrtg_GooglePlay.zip b/game_config/com.shape.shift.run.launcher.free.game.xjrtg/com.shape.shift.run.launcher.free.game.xjrtg_GooglePlay.zip new file mode 100644 index 0000000..a6da507 Binary files /dev/null and b/game_config/com.shape.shift.run.launcher.free.game.xjrtg/com.shape.shift.run.launcher.free.game.xjrtg_GooglePlay.zip differ diff --git a/scripts/context.py b/scripts/context.py index bde6f24..726d23b 100644 --- a/scripts/context.py +++ b/scripts/context.py @@ -9,6 +9,8 @@ class Context: repo_commit: str = "" package_name: str = "" + game_type: str = "" + project_original_path: str = "project/original" temp_project_path: str = "" @@ -16,7 +18,25 @@ class Context: local_repo_branch: str = "" local_repo_commit: str = "" + admob_app_id: str = "" + game_services_project_id: str = "" + facebook_app_id: str = "" + facebook_client_token: str = "" + + version_display_name: str = "1" + version_code: int = 1 + + config: any = None + @classmethod def from_json(cls, json_str: str): data = json.loads(json_str) return cls(**data) + + def get_config(self, key: str, default_value: str = '') -> str: + if self.config is None: + return default_value + return self.config.get(key, default_value).replace(" ", " ") + + def get_app_name(self): + return self.get_config("app_name") diff --git a/scripts/project_copy.py b/scripts/project_copy.py index f9a3b4a..7124184 100644 --- a/scripts/project_copy.py +++ b/scripts/project_copy.py @@ -1,5 +1,5 @@ from scripts.task import Task -from utils import FileUtils +from utils import FileUtils, TimeUtils from utils.logger_utils import app_logger @@ -14,6 +14,8 @@ class ProjectCopy(Task): def init(self): self.context.temp_project_path = self.context.project_original_path.replace("original", self.context.package_name.replace( - ".", "_")) + ".", "_") + + "_" + self.context.local_repo_commit + + "_" + TimeUtils.get_current_time_str()) pass diff --git a/scripts/project_update.py b/scripts/project_update.py index f98950a..bfa6082 100644 --- a/scripts/project_update.py +++ b/scripts/project_update.py @@ -1,6 +1,291 @@ +import os.path +from pathlib import Path +import re + +import javaproperties + +from scripts.context import Context from scripts.task import Task +from utils import FileUtils +from utils.logger_utils import app_logger + + +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 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 + + +GAME_ACTIVITY_PATH = f"LauncherCode/src/com/launchercode/GameActivity.kt".replace("/", os.sep) +ANDROID_MANIFEST_PATH = f"lawnchair/AndroidManifest.xml".replace("/", os.sep) class ProjectUpdate(Task): - def execute(self): + + 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, "build.gradle") + text = open(build_gradle_path, "r", encoding="utf-8").read() + + text = text.replace("com.fingerheart.launcher.game.free.sdjws", 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") + + # com.launchercode.SplashActivity + # TODO 这里还需要改启动项 + 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("com.fingerheart.launcher.game.free.sdjws", 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") + + google_services_json_path = os.path.join(dst, "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"lawnchair{os.sep}assets") + + for file in list(filter(lambda f: f != "google_fonts.json", 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(mainly_path, "tkg_config_mainly.properties"), 'rb') as f: + self.context.config = javaproperties.load(f) + + 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, "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) + FileUtils.decompress(res_path, temp_dst) + + if os.listdir(temp_dst).index("unityLibrary") >= 0: + FileUtils.copy(os.path.join(temp_dst, "unityLibrary"), dst) + else: + FileUtils.copy(temp_dst, dst) + + text = open(os.path.join(dst, "build.gradle"), "r", encoding="utf-8").read() + text = text.replace("implementation", "api") + open(os.path.join(dst, "build.gradle"), "w", encoding="utf-8").write(text) + + # 引用Unity项目 + text = open(self.build_gradle_path, "r", encoding="utf-8").read() + text = uncomment_line(text, "implementation projects.unityLibrary") + open(self.build_gradle_path, "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("WebActivity", "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("WebActivity", "com.unity3d.player.UnityPlayerActivity") + 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"LauncherCode{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): + 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, "build.gradle") + + text = open(build_gradle_path, "r", encoding="UTF-8").read() + + text = update_gradle_property(text, "admob_app_id", self.context.admob_app_id) + text = update_gradle_property(text, "game_services_project_id", self.context.game_services_project_id) + text = update_gradle_property(text, "facebook_app_id", self.context.facebook_app_id) + text = update_gradle_property(text, "facebook_client_token", self.context.facebook_client_token) + text = update_gradle_property(text, "derived_app_name", self.context.get_app_name()) + text = update_gradle_variable(text, "versionDisplayName", self.context.version_display_name) + text = update_gradle_variable(text, "version_code", self.context.version_code) + # text = text. + open(build_gradle_path, "w", encoding="UTF-8").write(text) + pass + + def execute(self): + 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() pass diff --git a/utils/__init__.py b/utils/__init__.py index 069d027..c97980c 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,5 +1,6 @@ from .system_utils import SystemUtils from .command_utils import CommandUtils from .file_utils import FileUtils +from .time_utils import TimeUtils -__all__ = ['SystemUtils', 'CommandUtils', "FileUtils"] +__all__ = ['SystemUtils', 'CommandUtils', "FileUtils", "TimeUtils"] diff --git a/utils/time_utils.py b/utils/time_utils.py new file mode 100644 index 0000000..c84d756 --- /dev/null +++ b/utils/time_utils.py @@ -0,0 +1,113 @@ +import time +import datetime +from enum import Enum + + +class TimeUtils: + """ + 时间工具类 + 功能: + 1. 获取当前时间的各种格式化输出 + 2. 时间转换 + 3. 时间计算 + """ + + class Weekday(Enum): + MONDAY = "星期一" + TUESDAY = "星期二" + WEDNESDAY = "星期三" + THURSDAY = "星期四" + FRIDAY = "星期五" + SATURDAY = "星期六" + SUNDAY = "星期日" + + @staticmethod + def get_current_time(): + """ + 获取当前时间,格式:年-月-日-时-分 + 返回:str,例如:2023-11-15-14-30 + """ + now = datetime.datetime.now() + return now.strftime("%Y-%m-%d-%H-%M") + + @staticmethod + def get_current_time_str(): + """ + 获取当前时间,格式:年-月-日-时-分 + 返回:str,例如:2023-11-15-14-30 + """ + now = datetime.datetime.now() + return now.strftime("%Y%m%d%H%M") + + @staticmethod + def get_current_time_with_weekday(): + """ + 获取当前时间和星期几 + 返回:str,例如:2023-11-15-14-30 星期三 + """ + now = datetime.datetime.now() + weekday = TimeUtils.Weekday((now.weekday() + 1) % 7 + 1).value + return f"{now.strftime('%Y-%m-%d-%H-%M')} {weekday}" + + @staticmethod + def get_timestamp(): + """ + 获取当前时间戳 + 返回:int,例如:1636969800 + """ + return int(time.time()) + + @staticmethod + def get_formatted_time(timestamp=None, format_str="%Y-%m-%d %H:%M:%S"): + """ + 格式化时间 + :param timestamp: 时间戳,默认为当前时间 + :param format_str: 格式字符串 + 返回:str,例如:2023-11-15 14:30:00 + """ + if timestamp is None: + timestamp = time.time() + return time.strftime(format_str, time.localtime(timestamp)) + + @staticmethod + def time_str_to_timestamp(time_str, format_str="%Y-%m-%d %H:%M:%S"): + """ + 时间字符串转时间戳 + :param time_str: 时间字符串 + :param format_str: 格式字符串 + 返回:int,例如:1636969800 + """ + time_array = time.strptime(time_str, format_str) + return int(time.mktime(time_array)) + + @staticmethod + def get_time_difference(start_time, end_time=None, format_str="%Y-%m-%d %H:%M:%S"): + """ + 计算两个时间的差值(秒) + :param start_time: 开始时间(字符串或datetime对象) + :param end_time: 结束时间(字符串或datetime对象),默认为当前时间 + :param format_str: 当参数为字符串时的格式 + 返回:timedelta对象 + """ + if isinstance(start_time, str): + start_time = datetime.datetime.strptime(start_time, format_str) + if end_time is None: + end_time = datetime.datetime.now() + elif isinstance(end_time, str): + end_time = datetime.datetime.strptime(end_time, format_str) + + return end_time - start_time + + +# 使用示例 +if __name__ == "__main__": + print("当前时间(年-月-日-时-分):", TimeUtils.get_current_time()) + print("当前时间和星期:", TimeUtils.get_current_time_with_weekday()) + print("当前时间戳:", TimeUtils.get_timestamp()) + print("格式化当前时间:", TimeUtils.get_formatted_time()) + + time_str = "2023-11-15 14:30:00" + print(f"'{time_str}' 转时间戳:", TimeUtils.time_str_to_timestamp(time_str)) + + start_time = "2023-11-15 14:30:00" + print(f"从 '{start_time}' 到现在的时间差:", TimeUtils.get_time_difference(start_time))