Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions plugins/ufc-scoreboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugins/ufc-scoreboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down
11 changes: 8 additions & 3 deletions plugins/ufc-scoreboard/generate_placeholder_icon.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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"))
8 changes: 7 additions & 1 deletion plugins/ufc-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ufc-scoreboard",
"name": "UFC Scoreboard",
"version": "1.19.3",
"version": "1.19.4",
"author": "LegoGuy1000",
"contributors": [
{
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 16 additions & 4 deletions plugins/ufc-scoreboard/scroll_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down
142 changes: 142 additions & 0 deletions plugins/ufc-scoreboard/test_separator_icons.py
Original file line number Diff line number Diff line change
@@ -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: <core-venv>/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())
4 changes: 2 additions & 2 deletions scripts/test_core_asset_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
Loading