#!/usr/bin/env python3 import os import sys import glob import re import shutil def read_file(path): with open(path, "r", encoding="utf-8") as f: return f.read() def write_file(path, content): with open(path, "w", encoding="utf-8") as f: f.write(content) def find_first(filename, repo_dir): results = glob.glob(os.path.join(repo_dir, "**", filename), recursive=True) if not results: sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}") return results[0] def copy_plugin_sources(plugin_dir, repo_dir): activities_dest = os.path.join(repo_dir, "src", "activities") os.makedirs(activities_dest, exist_ok=True) for fname in os.listdir(plugin_dir): if not fname.endswith((".h", ".cpp", ".hpp")): continue src = os.path.join(plugin_dir, fname) dst = os.path.join(activities_dest, fname) shutil.copy2(src, dst) print(f" ✓ {fname}") def patch_setting_action_enum(repo_dir): path = find_first("SettingsActivity.h", repo_dir) content = read_file(path) if "PongGame" in content: print(" SettingAction enum already patched, skipping.") return content = re.sub(r'(\n};)', r'\n PongGame,\1', content, count=1) write_file(path, content) print(" SettingAction::PongGame added.") def patch_settings_h(repo_dir): path = find_first("SettingsActivity.h", repo_dir) content = read_file(path) already_has_tab = "categoryCount = 5" in content already_has_vec = "pluginsSettings" in content if already_has_tab and already_has_vec: print(" SettingsActivity.h already patched, skipping.") return if not already_has_tab: content = content.replace( "static constexpr int categoryCount = 4;", "static constexpr int categoryCount = 5;" ) if not already_has_vec: content = content.replace( " std::vector systemSettings;", " std::vector systemSettings;\n std::vector pluginsSettings;" ) write_file(path, content) print(" SettingsActivity.h patched.") def patch_settings_cpp(repo_dir): path = find_first("SettingsActivity.cpp", repo_dir) content = read_file(path) if "PongActivity" in content and "SettingAction::PongGame" in content: print(" SettingsActivity.cpp already patched, skipping.") return content = content.replace( '#include "SettingsList.h"', '#include "SettingsList.h"\n#include "activities/PongActivity.h"' ) if "StrId::STR_NONE_OPT};" not in content: content = content.replace( "StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};", "StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n" " StrId::STR_NONE_OPT};" ) if "pluginsSettings.clear();" not in content: result = content.replace( " systemSettings.clear();\n", " systemSettings.clear();\n pluginsSettings.clear();\n" ) if "pluginsSettings.clear();" in result: content = result print(" pluginsSettings.clear() inserted.") else: print(" WARNING: could not insert pluginsSettings.clear().") pong_push = ( " pluginsSettings.push_back(SettingInfo::Action(\n" " StrId::STR_NONE_OPT, SettingAction::PongGame\n" " ));\n" ) if "SettingAction::PongGame" not in content: inserted = False for anchor in [ " selectedCategoryIndex = 0;\n selectedSettingIndex = 0;", " selectedCategoryIndex = 0;", " currentSettings = &displaySettings;", ]: if anchor in content: content = content.replace(anchor, pong_push + anchor, 1) inserted = True print(" PongGame push_back inserted.") break if not inserted: print(" WARNING: could not insert PongGame push_back.") if "case 4:\n currentSettings = &pluginsSettings;" not in content: result = content.replace( " case 3:\n currentSettings = &systemSettings;\n break;", " case 3:\n currentSettings = &systemSettings;\n break;\n" " case 4:\n currentSettings = &pluginsSettings;\n break;" ) if result != content: content = result print(" case 4 inserted.") else: print(" WARNING: case 3 anchor not found.") if '"Plugins"' not in content: result = content.replace( " tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n" " }", ' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n' " tabs.push_back({tabLabel, selectedCategoryIndex == i});\n" " }" ) if result != content: content = result print(" Plugins tab label inserted.") else: print(" WARNING: tab label anchor not found.") if "nextCatLabel" not in content and "nextCatIdx" not in content: result = content.replace( " const auto confirmLabel = (selectedSettingIndex == 0)\n" " ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])\n" " : tr(STR_TOGGLE);", " const int nextCatIndex = (selectedCategoryIndex + 1) % categoryCount;\n" ' const char* nextCatLabel = (nextCatIndex == 4) ? "Plugins" : I18N.get(categoryNames[nextCatIndex]);\n' " const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);" ) if result != content: content = result print(" nextCatLabel inserted.") else: print(" WARNING: confirmLabel anchor not found.") pong_label = ( " if (s.type == SettingType::ACTION &&\n" ' s.action == SettingAction::PongGame) return "Pong Game";\n' ) if "Pong Game" not in content: if "selectedCategoryIndex == 4" in content: result = re.sub( r'( if \(selectedCategoryIndex == 4\) \{.*?)( \})', lambda m: m.group(1) + pong_label + m.group(2), content, count=1, flags=re.DOTALL, ) if result != content: content = result print(" Pong Game label inserted.") else: print(" WARNING: category==4 block found but label insertion failed.") else: result = content.replace( " [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },", " [&settings, this](int index) -> std::string {\n" " if (selectedCategoryIndex == 4) {\n" " const auto& s = settings[index];\n" + pong_label + " }\n" " return std::string(I18N.get(settings[index].nameId));\n" " }," ) if result != content: content = result print(" Pong Game label inserted (new block).") else: print(" WARNING: label lambda anchor not found.") pong_value = ( " if (setting.type == SettingType::ACTION &&\n" " setting.action == SettingAction::PongGame) {\n" ' valueText = "Launch";\n' " } else " ) if '"Launch"' not in content: result = re.sub( r'( if \(setting\.type == SettingType::(?:ACTION|TOGGLE|ENUM)\b)', pong_value + r'\1', content, count=1, ) if result != content: content = result print(" Launch value text inserted.") else: print(" WARNING: value text anchor not found.") pong_case = ( " case SettingAction::PongGame:\n" " startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler);\n" " break;\n" ) if "case SettingAction::PongGame:" not in content: result = re.sub( r'( case SettingAction::)', pong_case + r'\1', content, count=1, ) if result != content: content = result print(" PongGame case inserted.") else: print(" WARNING: SettingAction case anchor not found.") write_file(path, content) print(" SettingsActivity.cpp written.") def patch(repo_dir: str): plugin_dir = os.path.dirname(os.path.abspath(__file__)) print(" Copying plugin sources...") copy_plugin_sources(plugin_dir, repo_dir) print(" Patching SettingAction enum...") patch_setting_action_enum(repo_dir) print(" Patching SettingsActivity.h...") patch_settings_h(repo_dir) print(" Patching SettingsActivity.cpp...") patch_settings_cpp(repo_dir) if __name__ == "__main__": if len(sys.argv) < 2: sys.exit("Usage: python patch.py ") patch(sys.argv[1])