From c6c94ebe78cb816f5d71922b356270f2eb551341 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:16:29 -0400 Subject: [PATCH] fix(ufc-scoreboard): load the separator icon the plugin ships (1.19.4) The scroll display only looked for assets/sports/ufc_logos/UFC.png relative to the working directory, the LEDMatrix install root on a Pi. The core has never shipped that file. The plugin has shipped its own copy since it was added (#24), but nothing read it, so every install logged "UFC separator icon not found" each time the scroll display was built (25 times in one harness run) and the Vegas ticker ran UFC's fights with no separator. Fall back to the plugin's copy, resolved from __file__. The core path is tried first, so a PNG there still overrides it; with neither, skip quietly at debug ("will skip separator"), as baseball, basketball, afl, nrl and soccer do. The core path stays referenced, so it stays in UNSHIPPED_SEPARATOR_ICONS; only its comment changes. generate_placeholder_icon.py now writes the bundled file instead of a cwd-relative one, and its docstring no longer implies a manual step. Co-Authored-By: Claude Opus 5.5 --- plugins.json | 7 +- plugins/ufc-scoreboard/CHANGELOG.md | 12 ++ plugins/ufc-scoreboard/README.md | 2 +- .../generate_placeholder_icon.py | 11 +- plugins/ufc-scoreboard/manifest.json | 8 +- plugins/ufc-scoreboard/scroll_display.py | 20 ++- .../ufc-scoreboard/test_separator_icons.py | 142 ++++++++++++++++++ scripts/test_core_asset_paths.py | 4 +- 8 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 plugins/ufc-scoreboard/test_separator_icons.py diff --git a/plugins.json b/plugins.json index 50a2cb15..72192db1 100644 --- a/plugins.json +++ b/plugins.json @@ -1004,13 +1004,12 @@ "plugin_path": "plugins/ufc-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-10-02", + "last_updated": "2026-10-04", "verified": true, "screenshot": "", - "latest_version": "1.19.3", + "latest_version": "1.19.4", "icon": "fas fa-fist-raised", - "ledmatrix_min_version": "3.8.0", - "commit": "886d2038d22a35c962beac0ea261543339cffec9" + "ledmatrix_min_version": "3.8.0" }, { "id": "weather", diff --git a/plugins/ufc-scoreboard/CHANGELOG.md b/plugins/ufc-scoreboard/CHANGELOG.md index 6089d88b..8e373fb8 100644 --- a/plugins/ufc-scoreboard/CHANGELOG.md +++ b/plugins/ufc-scoreboard/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.19.4] - 2026-10-04 + +### Fixed +- The Vegas ticker draws the UFC separator icon before the fight cards. The + icon has always shipped with the plugin, but the scroll display only looked + for it relative to the LEDMatrix install, where the core has never shipped + one. So there was no separator, and every install logged + `UFC separator icon not found` each time the scroll display was built. It + now falls back to the plugin's own copy. A PNG at + `assets/sports/ufc_logos/UFC.png` in the install still overrides it, and + if neither exists the separator is skipped with a debug line. + ## [1.19.3] - 2026-10-02 ### Changed diff --git a/plugins/ufc-scoreboard/README.md b/plugins/ufc-scoreboard/README.md index 70904382..0f29b373 100644 --- a/plugins/ufc-scoreboard/README.md +++ b/plugins/ufc-scoreboard/README.md @@ -254,7 +254,7 @@ not scroll. | `ufc.scroll_settings.scroll_speed` | 1.0–200.0 px/s | `50.0` | **Advanced.** Every value is pixels per second; higher scrolls faster. **Changed in 1.13.0:** this setting used to be ignored and the Vegas fight cards scrolled at 100 px/s; set `100` for that speed. | | `ufc.scroll_settings.scroll_delay` | 0.001–0.1 s | `0.01` | **Advanced.** Ignored; kept so saved configs still load. Scrolling is paced to the panel refresh; `scroll_speed` sets the speed. **Hidden from the config form since 1.15.0 (still declared).** | | `ufc.scroll_settings.gap_between_games` | 8–128 px | `48` | Gap between fight cards. | -| `ufc.scroll_settings.show_league_separators` | boolean | `true` | Draw the UFC icon between leagues. | +| `ufc.scroll_settings.show_league_separators` | boolean | `true` | Draw the UFC octagon icon before the fight cards. The icon ships with the plugin; a PNG at `assets/sports/ufc_logos/UFC.png` in the LEDMatrix install replaces it. **Before 1.19.4** the plugin only looked in the install, where nothing ships one, so no separator was drawn. | | `ufc.scroll_settings.dynamic_duration` | boolean | `true` | Size the scroll duration from the content width. | | `ufc.scroll_settings.game_card_width` | 32–512 px | `128` | Card width. Lower it on a multi-panel chain to fit more fights on screen at once. | diff --git a/plugins/ufc-scoreboard/generate_placeholder_icon.py b/plugins/ufc-scoreboard/generate_placeholder_icon.py index e099d38d..ec6c6cd7 100644 --- a/plugins/ufc-scoreboard/generate_placeholder_icon.py +++ b/plugins/ufc-scoreboard/generate_placeholder_icon.py @@ -1,11 +1,15 @@ """Generate a placeholder UFC separator icon for scroll display. -Run this script once to create assets/sports/ufc_logos/UFC.png. -Replace with an official UFC octagon logo when available. +The icon this writes ships with the plugin, at +assets/sports/ufc_logos/UFC.png next to this script, and the scroll display +loads it from there; nobody needs to run this to get a separator. Run it to +regenerate that file. A PNG at the same path under the LEDMatrix install +overrides it, if you want an official UFC logo instead. """ import os import math +from pathlib import Path from PIL import Image, ImageDraw, ImageFont @@ -99,4 +103,5 @@ def create_ufc_octagon_icon(output_path: str, size: int = 64): if __name__ == "__main__": - create_ufc_octagon_icon("assets/sports/ufc_logos/UFC.png") + create_ufc_octagon_icon( + str(Path(__file__).resolve().parent / "assets" / "sports" / "ufc_logos" / "UFC.png")) diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index c20bb73f..287fc46c 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.19.3", + "version": "1.19.4", "author": "LegoGuy1000", "contributors": [ { @@ -32,6 +32,12 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "version": "1.19.4", + "released": "2026-10-04", + "ledmatrix_min_version": "3.8.0", + "notes": "The Vegas ticker draws the UFC separator icon before the fight cards. The icon has always shipped with the plugin, but the scroll display only looked for it relative to the LEDMatrix install, where the core has never shipped one, so there was no separator and every install logged 'UFC separator icon not found' each time the scroll display was built. It now falls back to the plugin's own copy; a PNG at assets/sports/ufc_logos/UFC.png in the install still overrides it, and with neither the separator is skipped at DEBUG." + }, { "version": "1.19.3", "released": "2026-10-02", diff --git a/plugins/ufc-scoreboard/scroll_display.py b/plugins/ufc-scoreboard/scroll_display.py index 2a2d37dc..342961b4 100644 --- a/plugins/ufc-scoreboard/scroll_display.py +++ b/plugins/ufc-scoreboard/scroll_display.py @@ -59,8 +59,14 @@ class ScrollDisplayManager: - Implements dynamic duration based on total content width """ - # Path to UFC separator icon + # Path to UFC separator icon, relative to the core install (the cwd on a + # Pi). The core has never shipped one, so a file here is only an + # override. The default is the icon this plugin has always shipped, + # found relative to this file rather than the cwd: looking only here, + # every install logged a warning and scrolled with no separator. UFC_SEPARATOR_ICON = "assets/sports/ufc_logos/UFC.png" + BUNDLED_SEPARATOR_ICON = str( + Path(__file__).resolve().parent / "assets" / "sports" / "ufc_logos" / "UFC.png") def _scroll_frame_hold(self) -> int: """Refreshes to hold each frame for, from the resolved scroll settings.""" @@ -200,9 +206,12 @@ def _load_separator_icons(self) -> None: """Load and resize UFC separator icon.""" separator_height = self.display_height - 4 - if os.path.exists(self.UFC_SEPARATOR_ICON): + icon_path = next( + (p for p in (self.UFC_SEPARATOR_ICON, self.BUNDLED_SEPARATOR_ICON) + if os.path.exists(p)), None) + if icon_path: try: - with Image.open(self.UFC_SEPARATOR_ICON) as ufc_icon: + with Image.open(icon_path) as ufc_icon: if ufc_icon.mode != "RGBA": ufc_icon = ufc_icon.convert("RGBA") aspect = ufc_icon.width / ufc_icon.height @@ -215,7 +224,10 @@ def _load_separator_icons(self) -> None: except Exception as e: self.logger.error(f"Error loading UFC separator icon: {e}") else: - self.logger.warning(f"UFC separator icon not found at {self.UFC_SEPARATOR_ICON}") + # Only reachable if the bundled icon was deleted from the install. + self.logger.debug( + f"UFC separator icon not found at {self.UFC_SEPARATOR_ICON} or " + f"{self.BUNDLED_SEPARATOR_ICON} (will skip separator)") def _determine_fight_type(self, fight: Dict) -> str: """Determine fight type from its data.""" diff --git a/plugins/ufc-scoreboard/test_separator_icons.py b/plugins/ufc-scoreboard/test_separator_icons.py new file mode 100644 index 00000000..1e398d6d --- /dev/null +++ b/plugins/ufc-scoreboard/test_separator_icons.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Separator icon: the one this plugin ships loads, wherever the cwd is. + +The loader only looked for assets/sports/ufc_logos/UFC.png relative to the +working directory -- the core install root on a Pi -- and the core has never +shipped that file. The plugin has always shipped its own copy next to this +script, so every install logged a warning on every load of the scroll display +and the Vegas ticker ran UFC's fights with no separator. + +Builds the real ScrollDisplayManager from a scratch working directory, so the +result does not depend on what a core checkout has under assets/, and records +what it logs. Needs a core with src.common.sports_font_path (fight_renderer +imports it): LEDMATRIX_CORE, the working directory, or ../LEDMatrix next to +this repo; exits 2 without one. + +Run: /bin/python plugins/ufc-scoreboard/test_separator_icons.py +""" + +import logging +import os +import sys +import tempfile +from pathlib import Path +from types import SimpleNamespace + +from PIL import Image + +PLUGIN_DIR = Path(__file__).resolve().parent +CORE_ICON = Path("assets", "sports", "ufc_logos", "UFC.png") +BUNDLED_ICON = PLUGIN_DIR / CORE_ICON +#: Panel heights to build at: the separator is display_height - 4 tall. +HEIGHTS = (32, 64) + + +def core_checkout(): + for candidate in (os.environ.get("LEDMATRIX_CORE", ""), os.getcwd(), + str(PLUGIN_DIR.parents[2] / "LEDMatrix")): + if candidate and (Path(candidate) / "src" / "common" / "sports_font_path.py").is_file(): + return Path(candidate) + return None + + +class _Records(logging.Handler): + def __init__(self): + super().__init__(logging.DEBUG) + self.records = [] + + def emit(self, record): + self.records.append(record) + + +def build(mod, height): + """A ScrollDisplayManager at 128 x height, and what it logged.""" + logger = logging.getLogger("test_separator_icons.%d.%d" % (height, id(mod))) + logger.setLevel(logging.DEBUG) + logger.propagate = False + handler = _Records() + logger.handlers = [handler] + display = SimpleNamespace(matrix=None, width=128, height=height) + manager = mod.ScrollDisplayManager(display, {}, custom_logger=logger) + warned = [r.getMessage() for r in handler.records + if r.levelno >= logging.WARNING and "separator" in r.getMessage().lower()] + return manager, warned + + +def main(): + core = core_checkout() + if core is None: + print("SKIP: no LEDMatrix core checkout with src.common.sports_font_path") + return 2 + sys.path.insert(0, str(PLUGIN_DIR)) + sys.path.insert(1, str(core)) + + import scroll_display as mod + + failures = [] + + def check(name, cond, detail=""): + print((" PASS " if cond else " FAIL ") + name + + ("" if cond or not detail else ": " + detail)) + if not cond: + failures.append(name) + + with Image.open(BUNDLED_ICON) as bundled: + check("the bundled icon is a square RGBA PNG", bundled.mode == "RGBA" + and bundled.width == bundled.height, "%s %s" % (bundled.mode, bundled.size)) + + original_cwd = os.getcwd() + with tempfile.TemporaryDirectory() as scratch: + try: + os.chdir(scratch) + + print("bundled icon, from a working directory with no assets/") + for height in HEIGHTS: + manager, warned = build(mod, height) + icon = manager._separator_icons.get("ufc") + check("%dpx panel: ufc separator loads" % height, icon is not None, + "not in %s" % sorted(manager._separator_icons)) + if icon is not None: + check("%dpx panel: separator is %dx%d" % (height, height - 4, height - 4), + icon.size == (height - 4, height - 4), "got %s" % (icon.size,)) + check("%dpx panel: nothing about separators logged above debug" % height, + not warned, repr(warned)) + + print("a file at the core path overrides it") + (Path(scratch) / CORE_ICON).parent.mkdir(parents=True) + Image.new("RGBA", (40, 20), (0, 0, 255, 255)).save(Path(scratch) / CORE_ICON) + manager, warned = build(mod, 32) + icon = manager._separator_icons.get("ufc") + check("override is the one loaded (2:1, so 56x28)", + icon is not None and icon.size == (56, 28), + "got %s" % (icon.size if icon is not None else None,)) + check("override: nothing about separators logged above debug", + not warned, repr(warned)) + (Path(scratch) / CORE_ICON).unlink() + + print("neither file present") + cls = mod.ScrollDisplayManager + original = cls.__dict__.get("BUNDLED_SEPARATOR_ICON") + cls.BUNDLED_SEPARATOR_ICON = str(Path(scratch) / "missing.png") + try: + manager, warned = build(mod, 32) + finally: + if original is None: + del cls.BUNDLED_SEPARATOR_ICON + else: + cls.BUNDLED_SEPARATOR_ICON = original + check("no ufc separator", "ufc" not in manager._separator_icons) + check("skipped quietly: nothing about separators logged above debug", + not warned, repr(warned)) + finally: + os.chdir(original_cwd) + + if failures: + print("\n%d failure(s)" % len(failures)) + return 1 + print("\nall passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_core_asset_paths.py b/scripts/test_core_asset_paths.py index 925e1fbc..b003b49b 100644 --- a/scripts/test_core_asset_paths.py +++ b/scripts/test_core_asset_paths.py @@ -44,7 +44,7 @@ #: Separator icons a scoreboard names but the core does not ship. Each is a #: drop-in: the league scrolls without a separator until a file is added at -#: that path. A path NOT listed here +#: that path (ufc falls back to the icon it ships). A path NOT listed here #: must name a file the core ships -- hockey named ncaa_hockey.png for years #: while the core's NCAA hockey badge sat at ncaah.png. An entry that the core #: starts shipping fails too, so this list only shrinks. @@ -54,7 +54,7 @@ "assets/sports/wnba_logos/WNBA.png", # basketball: no WNBA league logo "assets/sports/ncaa_logos/NCAA.png", # hockey, lacrosse: no sport-neutral NCAA badge "assets/sports/ncaa_logos/ncaa_lacrosse.png", # lacrosse: no NCAA lacrosse badge - "assets/sports/ufc_logos/UFC.png", # ufc: generate_placeholder_icon.py writes one + "assets/sports/ufc_logos/UFC.png", # ufc: an override; the plugin ships its own copy } SEPARATOR_RE = re.compile(r"""^\s+\w+_SEPARATOR_ICON\s*=\s*["']([^"']+)["']""", re.M)