From ad280e6d5e0c424c76bc992580f4a905b77e7757 Mon Sep 17 00:00:00 2001 From: Justin Oros Date: Fri, 1 May 2026 07:18:28 -0700 Subject: [PATCH] Pong Game --- plugins/pong/PongActivity.cpp | 209 ++++++++++++++++++++++++++ plugins/pong/PongActivity.h | 50 +++++++ plugins/pong/patch.py | 269 ++++++++++++++++++++++++++++++++++ 3 files changed, 528 insertions(+) create mode 100644 plugins/pong/PongActivity.cpp create mode 100644 plugins/pong/PongActivity.h create mode 100644 plugins/pong/patch.py diff --git a/plugins/pong/PongActivity.cpp b/plugins/pong/PongActivity.cpp new file mode 100644 index 0000000..e12f951 --- /dev/null +++ b/plugins/pong/PongActivity.cpp @@ -0,0 +1,209 @@ +#include "PongActivity.h" + +#include +#include "MappedInputManager.h" +#include "fontIds.h" + +void PongActivity::onEnter() { + Activity::onEnter(); + + screenW_ = renderer.getScreenWidth(); + screenH_ = renderer.getScreenHeight(); + + playerX_ = (screenW_ - PADDLE_W) / 2.0f; + cpuX_ = (screenW_ - PADDLE_W) / 2.0f; + playerScore_ = 0; + cpuScore_ = 0; + gameOver_ = false; + playerWon_ = false; + lastUpdate_ = millis(); + + resetBall(true); + renderPending_ = true; + requestUpdate(); +} + +void PongActivity::onExit() { + Activity::onExit(); +} + +void PongActivity::resetBall(bool playerServes) { + ballX_ = screenW_ / 2.0f; + ballY_ = screenH_ / 2.0f; + ballDX_ = (playerServes ? 1.0f : -1.0f) * BALL_SPEED_X; + ballDY_ = BALL_SPEED_Y; +} + +void PongActivity::moveCpu() { + float cpuCenterX = cpuX_ + PADDLE_W / 2.0f; + float ballCenterX = ballX_ + BALL_SIZE / 2.0f; + + if (ballCenterX < cpuCenterX - 4) { + cpuX_ -= CPU_SPEED; + } else if (ballCenterX > cpuCenterX + 4) { + cpuX_ += CPU_SPEED; + } + + if (cpuX_ < MARGIN) cpuX_ = MARGIN; + if (cpuX_ + PADDLE_W > screenW_ - MARGIN) cpuX_ = screenW_ - MARGIN - PADDLE_W; +} + +void PongActivity::updateGame() { + ballX_ += ballDX_; + ballY_ += ballDY_; + + if (ballX_ <= MARGIN) { + ballX_ = MARGIN; + ballDX_ = -ballDX_; + } + if (ballX_ + BALL_SIZE >= screenW_ - MARGIN) { + ballX_ = screenW_ - MARGIN - BALL_SIZE; + ballDX_ = -ballDX_; + } + + int playerPaddleY = screenH_ - MARGIN - PADDLE_H; + if (ballDY_ > 0 && + ballY_ + BALL_SIZE >= playerPaddleY && + ballY_ + BALL_SIZE <= playerPaddleY + PADDLE_H + 4 && + ballX_ + BALL_SIZE >= playerX_ && + ballX_ <= playerX_ + PADDLE_W) + { + ballY_ = playerPaddleY - BALL_SIZE; + ballDY_ = -ballDY_; + float hitPos = (ballX_ + BALL_SIZE / 2.0f) - (playerX_ + PADDLE_W / 2.0f); + ballDX_ = hitPos / (PADDLE_W / 2.0f) * BALL_SPEED_X * 1.5f; + if (ballDX_ > BALL_SPEED_X * 2) ballDX_ = BALL_SPEED_X * 2; + if (ballDX_ < -BALL_SPEED_X * 2) ballDX_ = -BALL_SPEED_X * 2; + } + + int cpuPaddleY = MARGIN; + if (ballDY_ < 0 && + ballY_ <= cpuPaddleY + PADDLE_H && + ballY_ >= cpuPaddleY - 4 && + ballX_ + BALL_SIZE >= cpuX_ && + ballX_ <= cpuX_ + PADDLE_W) + { + ballY_ = cpuPaddleY + PADDLE_H; + ballDY_ = -ballDY_; + float hitPos = (ballX_ + BALL_SIZE / 2.0f) - (cpuX_ + PADDLE_W / 2.0f); + ballDX_ = hitPos / (PADDLE_W / 2.0f) * BALL_SPEED_X * 1.5f; + if (ballDX_ > BALL_SPEED_X * 2) ballDX_ = BALL_SPEED_X * 2; + if (ballDX_ < -BALL_SPEED_X * 2) ballDX_ = -BALL_SPEED_X * 2; + } + + if (ballY_ > screenH_) { + cpuScore_++; + if (cpuScore_ >= SCORE_TO_WIN) { + gameOver_ = true; + playerWon_ = false; + } else { + resetBall(false); + } + } + + if (ballY_ + BALL_SIZE < 0) { + playerScore_++; + if (playerScore_ >= SCORE_TO_WIN) { + gameOver_ = true; + playerWon_ = true; + } else { + resetBall(true); + } + } + + moveCpu(); +} + +void PongActivity::loop() { + if (gameOver_) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back) || + mappedInput.wasPressed(MappedInputManager::Button::Confirm) || + mappedInput.wasPressed(MappedInputManager::Button::Left) || + mappedInput.wasPressed(MappedInputManager::Button::Right) || + mappedInput.wasPressed(MappedInputManager::Button::Up) || + mappedInput.wasPressed(MappedInputManager::Button::Down) || + mappedInput.wasPressed(MappedInputManager::Button::Power)) { + finish(); + } + return; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Power)) { + while (mappedInput.isPressed(MappedInputManager::Button::Power)) { + mappedInput.update(); + delay(10); + } + finish(); + return; + } + + bool moved = false; + + bool goLeft = mappedInput.wasPressed(MappedInputManager::Button::Left) || + mappedInput.wasPressed(MappedInputManager::Button::Up) || + mappedInput.wasPressed(MappedInputManager::Button::Back); + bool goRight = mappedInput.wasPressed(MappedInputManager::Button::Right) || + mappedInput.wasPressed(MappedInputManager::Button::Down) || + mappedInput.wasPressed(MappedInputManager::Button::Confirm); + + if (goLeft) { + playerX_ -= PADDLE_W; + if (playerX_ < MARGIN) playerX_ = MARGIN; + moved = true; + } + if (goRight) { + playerX_ += PADDLE_W; + if (playerX_ + PADDLE_W > screenW_ - MARGIN) playerX_ = screenW_ - MARGIN - PADDLE_W; + moved = true; + } + + + unsigned long now = millis(); + if (now - lastUpdate_ >= 33) { + lastUpdate_ = now; + updateGame(); + renderPending_ = true; + requestUpdate(); + } else if (moved) { + renderPending_ = true; + requestUpdate(); + } +} + +void PongActivity::render(RenderLock&&) { + renderPending_ = false; + + renderer.clearScreen(); + + renderer.drawLine(MARGIN, MARGIN, screenW_ - MARGIN, MARGIN); + renderer.drawLine(MARGIN, screenH_ - MARGIN, screenW_ - MARGIN, screenH_ - MARGIN); + renderer.drawLine(MARGIN, MARGIN, MARGIN, screenH_ - MARGIN); + renderer.drawLine(screenW_ - MARGIN, MARGIN, screenW_ - MARGIN, screenH_ - MARGIN); + + int midY = screenH_ / 2; + for (int x = MARGIN + 4; x < screenW_ - MARGIN; x += 16) { + renderer.drawLine(x, midY, x + 8, midY); + } + + int cpuPaddleY = MARGIN; + renderer.fillRect((int)cpuX_, cpuPaddleY, PADDLE_W, PADDLE_H, true); + + int playerPaddleY = screenH_ - MARGIN - PADDLE_H; + renderer.fillRect((int)playerX_, playerPaddleY, PADDLE_W, PADDLE_H, true); + + renderer.fillRect((int)ballX_, (int)ballY_, BALL_SIZE, BALL_SIZE, true); + + char scoreStr[16]; + snprintf(scoreStr, sizeof(scoreStr), "%d %d", cpuScore_, playerScore_); + int scoreX = screenW_ / 2 - 20; + renderer.drawText(UI_10_FONT_ID, scoreX, midY - 20, scoreStr, true); + + if (gameOver_) { + int quarterY = screenH_ / 4; + const char* msg = playerWon_ ? "YOU WIN!" : "CPU WINS"; + renderer.drawText(UI_12_FONT_ID, screenW_ / 2 - 50, quarterY - 20, msg, true, EpdFontFamily::BOLD); + renderer.drawText(UI_10_FONT_ID, screenW_ / 2 - 80, quarterY + 20, "Press any button to exit", true); + } + + renderer.displayBuffer(); +} diff --git a/plugins/pong/PongActivity.h b/plugins/pong/PongActivity.h new file mode 100644 index 0000000..469407f --- /dev/null +++ b/plugins/pong/PongActivity.h @@ -0,0 +1,50 @@ +#pragma once + +#include "activities/Activity.h" + +class PongActivity final : public Activity { +public: + explicit PongActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("Pong", renderer, mappedInput) {} + + void onEnter() override; + void onExit() override; + void loop() override; + bool skipLoopDelay() override { return true; } + void render(RenderLock&&) override; + +private: + static constexpr int PADDLE_W = 80; + static constexpr int PADDLE_H = 12; + static constexpr int PADDLE_SPEED = 8; + static constexpr int BALL_SIZE = 12; + static constexpr int BALL_SPEED_X = 4; + static constexpr int BALL_SPEED_Y = 4; + static constexpr int MARGIN = 20; + static constexpr int CPU_SPEED = 3; + static constexpr int SCORE_TO_WIN = 7; + + int screenW_ = 0; + int screenH_ = 0; + + float playerX_ = 0; + float cpuX_ = 0; + + float ballX_ = 0; + float ballY_ = 0; + float ballDX_ = 0; + float ballDY_ = 0; + + int playerScore_ = 0; + int cpuScore_ = 0; + + bool gameOver_ = false; + bool playerWon_ = false; + bool renderPending_ = false; + + unsigned long lastUpdate_ = 0; + + void resetBall(bool playerServes); + void updateGame(); + void moveCpu(); +}; diff --git a/plugins/pong/patch.py b/plugins/pong/patch.py new file mode 100644 index 0000000..b10cd03 --- /dev/null +++ b/plugins/pong/patch.py @@ -0,0 +1,269 @@ +#!/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])