diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b7a35a2..e9846c6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,6 +4,8 @@ This project is for developing applications on the GitHub Universe 2025 hackable ## Project Structure +**Default launcher structure:** + ``` / ├── badge/ # Badge firmware and apps (deployed to /system/ on device) @@ -24,6 +26,22 @@ This project is for developing applications on the GitHub Universe 2025 hackable └── README.md ``` +**With menu subfolder support enabled:** + +``` +badge/apps/ +├── games/ # Folder without __init__.py; appears as a submenu +│ ├── flappy/ # App (has __init__.py, icon.png) +│ └── snake/ +├── utilities/ +│ ├── wifi/ +│ └── weather/ +├── menu/ # Launcher logic (hidden from menu list) +└── startup/ # Boot animation (hidden from menu list) +``` + +When this structure is active, the launcher treats any directory lacking an `__init__.py` as a navigable folder, pins a back entry at the first slot inside subfolders, and continues to hide the `menu` and `startup` apps from the user-facing list. + ## Hardware Specifications - **Processor**: RP2350 Dual-core ARM Cortex-M33 @ 200MHz diff --git a/badge/apps/badge/__init__.py b/badge/apps/badge/__init__.py index 4079fb0..164a747 100755 --- a/badge/apps/badge/__init__.py +++ b/badge/apps/badge/__init__.py @@ -1,8 +1,15 @@ import sys import os -sys.path.insert(0, "/system/apps/badge") -os.chdir("/system/apps/badge") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/badge") from badgeware import io, brushes, shapes, Image, run, PixelFont, screen, Matrix, file_exists @@ -11,7 +18,6 @@ import network from urllib.urequest import urlopen import gc -import sys import json @@ -51,21 +57,27 @@ def get_connection_details(user): # `secrets.py` can be imported on the device. Clean up sys.path afterwards. sys.path.insert(0, "/") try: - from secrets import WIFI_PASSWORD, WIFI_SSID, GITHUB_USERNAME, GITHUB_TOKEN + secrets = __import__("secrets") finally: # ensure we remove the path we inserted even if import fails try: sys.path.pop(0) except Exception: pass - except ImportError as e: + + WIFI_PASSWORD = getattr(secrets, "WIFI_PASSWORD", None) + WIFI_SSID = getattr(secrets, "WIFI_SSID", None) + GITHUB_USERNAME = getattr(secrets, "GITHUB_USERNAME", None) + # Allow the token to be omitted entirely – treat missing as None/empty string + GITHUB_TOKEN = getattr(secrets, "GITHUB_TOKEN", None) + except ImportError: # If the user hasn't created a secrets.py file, fall back to None so # the rest of the app can detect missing credentials and show helpful UI. WIFI_PASSWORD = None WIFI_SSID = None GITHUB_USERNAME = None GITHUB_TOKEN = None - except Exception as e: + except Exception: WIFI_PASSWORD = None WIFI_SSID = None GITHUB_USERNAME = None diff --git a/badge/apps/copilot-loop/__init__.py b/badge/apps/copilot-loop/__init__.py index c22783b..1bd5212 100644 --- a/badge/apps/copilot-loop/__init__.py +++ b/badge/apps/copilot-loop/__init__.py @@ -1,11 +1,16 @@ import sys -import os -sys.path.insert(0, "/system/apps/copilot-loop") -os.chdir("/system/apps/copilot-loop") +if "/system" not in sys.path: + sys.path.insert(0, "/system") -from badgeware import Image, screen, run, io +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path +APP_DIR = ensure_app_path(globals(), "/system/apps/copilot-loop") + +from badgeware import io, run, screen, Image frame_index = 1 diff --git a/badge/apps/crypto/__init__.py b/badge/apps/crypto/__init__.py index 6d93533..ec9d786 100644 --- a/badge/apps/crypto/__init__.py +++ b/badge/apps/crypto/__init__.py @@ -1,11 +1,15 @@ import sys -import os +if "/system" not in sys.path: + sys.path.insert(0, "/system") -sys.path.insert(0, "/system/apps/crypto") -os.chdir("/system/apps/crypto") +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path -from badgeware import io, brushes, shapes, screen, PixelFont, run -import network +APP_DIR = ensure_app_path(globals(), "/system/apps/crypto") + +from badgeware import io, brushes, shapes, screen, PixelFont, Image, run from urllib.urequest import urlopen import json import gc diff --git a/badge/apps/flappy/__init__.py b/badge/apps/flappy/__init__.py index 00e92c5..8cf20f8 100755 --- a/badge/apps/flappy/__init__.py +++ b/badge/apps/flappy/__init__.py @@ -1,8 +1,14 @@ import sys -import os -sys.path.insert(0, "/system/apps/flappy") -os.chdir("/system/apps/flappy") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/flappy") from badgeware import screen, Image, PixelFont, SpriteSheet, io, brushes, shapes, run, State from mona import Mona diff --git a/badge/apps/gallery/__init__.py b/badge/apps/gallery/__init__.py index edfdf09..692df94 100755 --- a/badge/apps/gallery/__init__.py +++ b/badge/apps/gallery/__init__.py @@ -1,8 +1,15 @@ import sys import os -sys.path.insert(0, "/system/apps/gallery") -os.chdir("/system/apps/gallery") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/gallery") from badgeware import PixelFont, Image, screen, run, io, brushes, shapes diff --git a/badge/apps/invaders/__init__.py b/badge/apps/invaders/__init__.py index 79038f6..c5472d2 100755 --- a/badge/apps/invaders/__init__.py +++ b/badge/apps/invaders/__init__.py @@ -1,9 +1,15 @@ import sys -import os import random -sys.path.insert(0, "/system/apps/invaders") -os.chdir("/system/apps/invaders") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/invaders") from badgeware import screen, PixelFont, io, brushes, shapes, run diff --git a/badge/apps/menu/__init__.py b/badge/apps/menu/__init__.py index 64bb349..c1578b2 100755 --- a/badge/apps/menu/__init__.py +++ b/badge/apps/menu/__init__.py @@ -4,115 +4,225 @@ sys.path.insert(0, "/system/apps/menu") os.chdir("/system/apps/menu") -import math -from badgeware import screen, PixelFont, Image, SpriteSheet, is_dir, file_exists, shapes, brushes, io, run -from icon import Icon +from badgeware import screen, PixelFont, SpriteSheet, is_dir, file_exists, shapes, brushes, io, run +from icon import Icon, sprite_for import ui mona = SpriteSheet("/system/assets/mona-sprites/mona-default.png", 11, 1) screen.font = PixelFont.load("/system/assets/fonts/ark.ppf") # screen.antialias = Image.X2 -# Auto-discover apps with __init__.py -apps = [] -try: - for entry in os.listdir("/system/apps"): - app_path = f"/system/apps/{entry}" - if is_dir(app_path): - has_init = file_exists(f"{app_path}/__init__.py") - if has_init: - # Skip menu and startup apps - if entry not in ("menu", "startup"): - # Use directory name as display name - apps.append((entry, entry)) -except Exception as e: - print(f"Error discovering apps: {e}") +ROOT = "/system/apps" +current_path = ROOT +path_stack = [] +current_entries = [] +current_page = 0 +total_pages = 1 +icons = [] +active = 0 + + +FOLDER_SCAN_DEPTH = 4 + + +def folder_has_entries(path, depth=0): + if depth >= FOLDER_SCAN_DEPTH: + return False + try: + for name in os.listdir(path): + if name.startswith("."): + continue + child_path = "{}/{}".format(path, name) + if is_dir(child_path): + if file_exists("{}/__init__.py".format(child_path)): + return True + if folder_has_entries(child_path, depth + 1): + return True + except OSError as exc: + print("folder_has_entries failed for {}: {}".format(path, exc)) + return False + + +def scan_entries(root): + entries = [] + try: + for name in os.listdir(root): + if name.startswith("."): + continue + full_path = "{}/{}".format(root, name) + if is_dir(full_path): + has_module = file_exists("{}/__init__.py".format(full_path)) + if has_module: + entry = {"name": name, "path": full_path, "kind": "app"} + icon_path = "{}/icon.png".format(full_path) + if file_exists(icon_path): + entry["icon"] = icon_path + entries.append(entry) + elif folder_has_entries(full_path): + entries.append({"name": name, "path": full_path, "kind": "folder"}) + except OSError as exc: + print("scan_entries failed for {}: {}".format(root, exc)) + folders = [item for item in entries if item["kind"] == "folder"] + apps_only = [item for item in entries if item["kind"] == "app"] + folders.sort(key=lambda item: item["name"]) + apps_only.sort(key=lambda item: item["name"]) + return folders + apps_only + # Pagination constants APPS_PER_PAGE = 6 -current_page = 0 -total_pages = max(1, math.ceil(len(apps) / APPS_PER_PAGE)) -# find installed apps and create icons for current page + +def current_dir(): + if not path_stack: + return ROOT + return path_stack[-1] + + +def rebuild_entries(reset_page): + global current_entries, current_page, total_pages, icons, active, current_path + current_path = current_dir() + entries = scan_entries(current_path) + if current_path == ROOT: + filtered = [] + for entry in entries: + if entry["name"] in ("menu", "startup"): + continue + filtered.append(entry) + entries = filtered + if path_stack: + entries.insert(0, {"name": "..", "path": current_path, "kind": "back"}) + current_entries = entries + total_pages = (len(current_entries) + APPS_PER_PAGE - 1) // APPS_PER_PAGE + if total_pages < 1: + total_pages = 1 + if reset_page: + current_page = 0 + elif current_page >= total_pages: + current_page = total_pages - 1 + start_idx = current_page * APPS_PER_PAGE + page_len = len(current_entries) - start_idx + if page_len < 0: + page_len = 0 + if page_len > APPS_PER_PAGE: + page_len = APPS_PER_PAGE + if page_len == 0: + active = 0 + elif active >= page_len: + active = page_len - 1 + icons = load_page_icons(current_page) + + def load_page_icons(page): - icons = [] + result = [] start_idx = page * APPS_PER_PAGE - end_idx = min(start_idx + APPS_PER_PAGE, len(apps)) - - for i in range(start_idx, end_idx): - app = apps[i] - name, path = app[0], app[1] - - if is_dir(f"/system/apps/{path}"): - icon_idx = i - start_idx - x = icon_idx % 3 - y = math.floor(icon_idx / 3) - pos = (x * 48 + 33, y * 48 + 42) - try: - # Try to load app-specific icon, fall back to default - icon_path = f"/system/apps/{path}/icon.png" - if not file_exists(icon_path): - icon_path = "/system/apps/menu/default_icon.png" - sprite = Image.load(icon_path) - icons.append(Icon(pos, name, icon_idx % APPS_PER_PAGE, sprite)) - except Exception as e: - print(f"Error loading icon for {path}: {e}") - return icons - -icons = load_page_icons(current_page) + end_idx = min(start_idx + APPS_PER_PAGE, len(current_entries)) + for offset in range(start_idx, end_idx): + entry = current_entries[offset] + slot = offset - start_idx + x = slot % 3 + y = slot // 3 + pos = (x * 48 + 33, y * 48 + 42) + try: + sprite = sprite_for(entry) + result.append(Icon(pos, entry["name"], slot % APPS_PER_PAGE, sprite)) + except Exception as e: + print("Error loading icon for {}: {}".format(entry.get("path", ""), e)) + return result -active = 0 + +def selected_entry(): + index = current_page * APPS_PER_PAGE + active + if 0 <= index < len(current_entries): + return current_entries[index] + return None + + +def enter_folder(entry): + if not is_dir(entry["path"]): + return + path_stack.append(entry["path"]) + rebuild_entries(True) + + +def leave_folder(): + if path_stack: + path_stack.pop() + rebuild_entries(True) MAX_ALPHA = 255 alpha = 30 +rebuild_entries(True) + + def update(): global active, icons, alpha, current_page, total_pages # process button inputs to switch between icons + if io.BUTTON_HOME in io.pressed: + if path_stack: + path_stack[:] = [] + rebuild_entries(True) + return None + + move_delta = 0 + if io.BUTTON_C in io.pressed: - active += 1 - if io.BUTTON_A in io.pressed: - active -= 1 + move_delta += 1 if io.BUTTON_UP in io.pressed: - active -= 3 + move_delta -= 3 if io.BUTTON_DOWN in io.pressed: - active += 3 + move_delta += 3 + + if io.BUTTON_A in io.pressed: + move_delta -= 1 + + if move_delta: + active += move_delta # Handle wrapping and page changes - if active >= len(icons): - if current_page < total_pages - 1: - # Move to next page - current_page += 1 - icons = load_page_icons(current_page) - active = 0 - else: - # Wrap to beginning - active = 0 - elif active < 0: - if current_page > 0: - # Move to previous page - current_page -= 1 - icons = load_page_icons(current_page) - active = len(icons) - 1 - else: - # Wrap to end - active = len(icons) - 1 - - # Launch app with error handling + page_len = len(icons) + if page_len == 0: + active = 0 + else: + if active >= page_len: + if current_page < total_pages - 1: + current_page += 1 + icons = load_page_icons(current_page) + active = 0 + page_len = len(icons) + else: + active = 0 + elif active < 0: + if current_page > 0: + current_page -= 1 + icons = load_page_icons(current_page) + page_len = len(icons) + active = page_len - 1 if page_len else 0 + else: + active = page_len - 1 + + entry = selected_entry() + + # Launch app or navigate with B if io.BUTTON_B in io.pressed: - app_idx = current_page * APPS_PER_PAGE + active - if app_idx < len(apps): - app_path = f"/system/apps/{apps[app_idx][1]}" - try: - # Verify the app still exists before launching - if is_dir(app_path) and file_exists(f"{app_path}/__init__.py"): - return app_path - else: - print(f"Error: App {apps[app_idx][1]} not found or missing __init__.py") - except Exception as e: - print(f"Error launching app {apps[app_idx][1]}: {e}") + if entry: + if entry["kind"] == "folder": + enter_folder(entry) + return None + if entry["kind"] == "back": + leave_folder() + return None + if entry["kind"] == "app": + app_path = entry["path"] + try: + if is_dir(app_path) and file_exists("{}/__init__.py".format(app_path)): + return app_path + print("Error: App {} not found or missing __init__.py".format(entry["name"])) + except Exception as exc: + print("Error launching app {}: {}".format(entry["name"], exc)) ui.draw_background() ui.draw_header() diff --git a/badge/apps/menu/folder_icon.png b/badge/apps/menu/folder_icon.png new file mode 100644 index 0000000..25b3123 Binary files /dev/null and b/badge/apps/menu/folder_icon.png differ diff --git a/badge/apps/menu/folder_up_icon.png b/badge/apps/menu/folder_up_icon.png new file mode 100644 index 0000000..53058e3 Binary files /dev/null and b/badge/apps/menu/folder_up_icon.png differ diff --git a/badge/apps/menu/icon.py b/badge/apps/menu/icon.py index 747c94b..263df4e 100755 --- a/badge/apps/menu/icon.py +++ b/badge/apps/menu/icon.py @@ -1,5 +1,5 @@ import math -from badgeware import brushes, shapes, io, Matrix, screen +from badgeware import brushes, shapes, io, Matrix, screen, Image # bright icon colours bold = [ @@ -26,6 +26,30 @@ squircle = shapes.squircle(0, 0, 20, 4) shade_brush = brushes.color(0, 0, 0, 30) +ICON_SPRITES = { + "app": "/system/apps/menu/default_icon.png", + "folder": "/system/apps/menu/folder_icon.png", + "back": "/system/apps/menu/folder_up_icon.png", +} + + +def sprite_for(entry): + custom_path = entry.get("icon") + if custom_path: + try: + return Image.load(custom_path) + except OSError: + pass + + path = ICON_SPRITES.get(entry.get("kind"), ICON_SPRITES["app"]) + try: + return Image.load(path) + except OSError: + try: + return Image.load(ICON_SPRITES["app"]) + except OSError: + return Image(24, 24) + class Icon: active_icon = None diff --git a/badge/apps/monapet/__init__.py b/badge/apps/monapet/__init__.py index 4e66762..62649a0 100755 --- a/badge/apps/monapet/__init__.py +++ b/badge/apps/monapet/__init__.py @@ -1,9 +1,14 @@ import sys -import os -sys.path.insert(0, "/system/apps/monapet") -os.chdir("/system/apps/monapet") +if "/system" not in sys.path: + sys.path.insert(0, "/system") +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/monapet") import ui from mona import Mona diff --git a/badge/apps/monapet/mona.py b/badge/apps/monapet/mona.py index 0255e09..b952635 100755 --- a/badge/apps/monapet/mona.py +++ b/badge/apps/monapet/mona.py @@ -1,8 +1,14 @@ import sys -import os -sys.path.insert(0, "/system/apps/monapet") -os.chdir("/system/apps/monapet") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/monapet") from badgeware import screen, brushes, SpriteSheet, shapes, clamp, io import random diff --git a/badge/apps/quest/__init__.py b/badge/apps/quest/__init__.py index 4b51596..ed5cfdd 100755 --- a/badge/apps/quest/__init__.py +++ b/badge/apps/quest/__init__.py @@ -1,12 +1,16 @@ import sys -import os -sys.path.insert(0, "/system/apps/quest") -os.chdir("/system/apps/quest") +if "/system" not in sys.path: + sys.path.insert(0, "/system") -import math -import random -from badgeware import State, PixelFont, Image, brushes, screen, io, shapes, run +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/quest") + +from badgeware import io, run, shapes, screen, PixelFont, SpriteSheet, Image, brushes, State from beacon import GithubUniverseBeacon from aye_arr.nec import NECReceiver import ui diff --git a/badge/apps/sketch/__init__.py b/badge/apps/sketch/__init__.py index 635f3da..2ac042f 100755 --- a/badge/apps/sketch/__init__.py +++ b/badge/apps/sketch/__init__.py @@ -1,11 +1,17 @@ import sys -import os -sys.path.insert(0, "/system/apps/sketch") -os.chdir("/system/apps/sketch") +if "/system" not in sys.path: + sys.path.insert(0, "/system") -from badgeware import Image, brushes, shapes, screen, io, run -import ui +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/sketch") + +from . import ui +from badgeware import io, brushes, shapes, Image, run, PixelFont, screen canvas = Image(0, 0, ui.canvas_area[2], ui.canvas_area[3]) diff --git a/badge/apps/stocks/__init__.py b/badge/apps/stocks/__init__.py index 022719f..820ae50 100644 --- a/badge/apps/stocks/__init__.py +++ b/badge/apps/stocks/__init__.py @@ -1,8 +1,14 @@ import sys -import os -sys.path.insert(0, "/system/apps/stocks") -os.chdir("/system/apps/stocks") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/stocks") from badgeware import io, brushes, shapes, screen, PixelFont, run import network diff --git a/badge/apps/weather/__init__.py b/badge/apps/weather/__init__.py index f1235d0..792c929 100644 --- a/badge/apps/weather/__init__.py +++ b/badge/apps/weather/__init__.py @@ -1,8 +1,14 @@ import sys -import os -sys.path.insert(0, "/system/apps/weather") -os.chdir("/system/apps/weather") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/weather") from badgeware import io, brushes, shapes, screen, PixelFont, run import network diff --git a/badge/apps/wifi/__init__.py b/badge/apps/wifi/__init__.py index db79456..ff1f9ae 100644 --- a/badge/apps/wifi/__init__.py +++ b/badge/apps/wifi/__init__.py @@ -1,8 +1,14 @@ import sys -import os -sys.path.insert(0, "/system/apps/wifi") -os.chdir("/system/apps/wifi") +if "/system" not in sys.path: + sys.path.insert(0, "/system") + +try: + from badge_app_runtime import ensure_app_path +except ImportError: + from badge_app_runtime import prepare_app_path as ensure_app_path + +APP_DIR = ensure_app_path(globals(), "/system/apps/wifi") from badgeware import io, brushes, shapes, screen, PixelFont, run, Matrix import network diff --git a/badge/badge_app_runtime.py b/badge/badge_app_runtime.py new file mode 100644 index 0000000..b5b4faf --- /dev/null +++ b/badge/badge_app_runtime.py @@ -0,0 +1,108 @@ +import os +import sys + + +_CACHE_KEY = "__badge_app_runtime_dir" + + +def _normalize_path(value): + if value is None: + return None + if isinstance(value, bytes): + value = value.decode() + value = str(value) + value = value.replace("\\", "/") + if value.endswith("/") and value != "/": + value = value[:-1] + return value or None + + +def _parent_dir(path): + path = _normalize_path(path) + if not path: + return None + slash = path.rfind("/") + if slash == -1: + return None + if slash == 0: + return "/" + return path[:slash] + + +def _dir_exists(path): + if not path: + return False + try: + os.listdir(path) + return True + except OSError: + return False + + +def _default_app_dir(module_globals, fallback): + module_name = module_globals.get("__name__", "") + if not module_name or module_name == "__main__": + return _normalize_path(fallback) + tail = module_name.rsplit(".", 1)[-1] + base = _normalize_path(fallback) + if base and base.endswith(tail): + return base + if base: + return base + return "/system/apps/{}".format(tail) + + +def ensure_app_path(module_globals, fallback=None): + cached = module_globals.get(_CACHE_KEY) + if cached: + return cached + + candidates = [] + + def add_candidate(path): + path = _normalize_path(path) + if path and path not in candidates: + candidates.append(path) + + add_candidate(_parent_dir(module_globals.get("__file__"))) + + module_spec = module_globals.get("__spec__") + if module_spec is not None: + add_candidate(_parent_dir(getattr(module_spec, "origin", None))) + + if sys.path: + add_candidate(sys.path[0]) + + add_candidate(_default_app_dir(module_globals, fallback)) + + app_dir = None + for candidate in candidates: + if not candidate: + continue + if _dir_exists(candidate): + app_dir = candidate + break + if app_dir is None: + app_dir = candidate + + if app_dir and app_dir not in sys.path: + sys.path.insert(0, app_dir) + + if app_dir: + try: + os.chdir(app_dir) + except OSError: + # It is safe to ignore errors when changing directory; app_dir may not exist or be accessible, + # but we still want to proceed with app path setup for compatibility. + pass + + module_globals[_CACHE_KEY] = app_dir + return app_dir + + +def prepare_app_path(module_globals, fallback=None): + """Backward-compatible wrapper for older apps.""" + return ensure_app_path(module_globals, fallback) + + +active_path = None diff --git a/simulator/badge_simulator.py b/simulator/badge_simulator.py index d9fc320..8eb4f88 100644 --- a/simulator/badge_simulator.py +++ b/simulator/badge_simulator.py @@ -1366,10 +1366,30 @@ def load_game_module(module_path: str) -> ModuleType: game_dir = os.path.dirname(game_abs) sim_root = SIM_ROOT if SIM_ROOT is not None else _find_sim_root(game_dir) simulator_dir = os.path.dirname(os.path.abspath(__file__)) - for p in (game_dir, os.path.join(sim_root, "apps"), simulator_dir): - if p not in sys.path: + search_paths = [game_dir, simulator_dir] + if sim_root: + search_paths.extend([ + os.path.join(sim_root, "apps"), + sim_root, + ]) + for p in search_paths: + if p and p not in sys.path: sys.path.insert(0, p) + # Ensure badge_app_runtime helper is available to imported apps + runtime_module = sys.modules.get("badge_app_runtime") + if runtime_module is None and sim_root: + runtime_path = os.path.join(sim_root, "badge_app_runtime.py") + if os.path.isfile(runtime_path): + runtime_spec = importlib.util.spec_from_file_location("badge_app_runtime", runtime_path) + if runtime_spec and runtime_spec.loader: + runtime_module = importlib.util.module_from_spec(runtime_spec) + runtime_spec.loader.exec_module(runtime_module) # type: ignore + sys.modules["badge_app_runtime"] = runtime_module + + if runtime_module is not None: + runtime_module.active_path = os.path.abspath(game_dir) + # Provide `badgeware` badgeware = ModuleType("badgeware") badgeware.screen = screen