Skip to content

Comments

feat(game): add screenshot media library infrastructure#4585

Open
wescopeland wants to merge 2 commits intoRetroAchievements:masterfrom
wescopeland:screenshot-media-library
Open

feat(game): add screenshot media library infrastructure#4585
wescopeland wants to merge 2 commits intoRetroAchievements:masterfrom
wescopeland:screenshot-media-library

Conversation

@wescopeland
Copy link
Member

@wescopeland wescopeland commented Feb 22, 2026

Games currently support a single ingame screenshot and a single title screenshot, stored as varchar columns on the games table. Uploads are capped at 1MB and forcibly downscaled to 320x240 via GD. Additionally, the original file is destroyed, meaning we are deliberately incurring data loss.

This PR creates the infrastructure for multiple screenshots per game using Spatie MediaLibrary, adds a dedicated Media page for GameResource in Filament, and preserves backward compatibility for all existing consumers of the current image assets in the games table.

Architecture Explanation

game_screenshots table + GameScreenshot model

See migration

Screenshot business logic gets its own dedicated table. Each row has a type (title / ingame / completion), primary flag, approval status, and ordering via order_column.

completion is a new type that is meant to serve as the opposite of a title screen - it's a screen showing "The End" that may eventually be used for user2 beaten showcases.

The primary flag is used to determine what the "canonical" ingame image is when multiple uploads are supported in a subsequent PR.

The approval status is used to support an approval queue, facilitating player uploads of content, which will also be handled in a subsequent PR.

System resolution metadata

A JSON screenshot_resolutions column on systems stores valid native output resolutions per system (eg: SNES = 256x224, Saturn = 14 valid resolutions). A boolean supports_resolution_scaling column flags systems whose emulators support internal resolution scaling (PSP, PS1, PS2, N64, GameCube, Wii, Dreamcast, DS, DSi, and 3DS). Upload validation checks exact resolution matches and accepts 2x/3x integer multiples for scaling-enabled systems. Systems will null resolutions (computers, arcade) allow any dimensions.

Upload flow

All uploads go through a new AddGameScreenshotAction, which validates file constraints (4MB max, 64x64 min, 1920x1080 max, PNG/JPEG/WebP only, no animation), checks SHA1 for duplicates and known placeholders, enforces per-type caps (20 ingame, 1 title, 1 completion), and validates resolution against the system.

For this piece of work, every Filament upload sets the new screenshot as primary for the type, which matches current production behavior where each upload replaces the previous one. "Demoted" screenshots are set to pending status to be reviewed later when multiple files are supported in a subsequent PR.

Backwards compatibility

GameScreenshotObserver automatically syncs the primary screenshot's legacy path back to the image_ingame_asset_path column on the games table. All existing consumers (V1 API, game pages, etc) continue working without changes.

Conversions

Original files are now stored on S3. Spatie generates sm (320px), md (640px), and lg (1280px) conversions in both WebP and AVIF. Small images are never upscaled due to the code using Fit::Max. These conversions aren't rendered in the React front-end yet.

A subsequent PR will include a command to backfill all existing legacy images to match these conversions, and flag the ones that fail to meet system-level resolution constraints.

Media Page

As mentioned above, a new "Media" tab on GameResource consolidates all game media management: badge, box art, title screenshot, banner, and ingame screenshots. The media section has been removed from the Edit page.

How to test this PR

# adds new game_screenshots table
# updates systems table with new columns
# seeds resolution metadata for dozens of systems
sail artisan migrate

# restart horizon
sail artisan horizon

Similar to the banners PR, ensure these are in your .env:

MEDIA_URL=http://localhost:${APP_PORT}/media

AWS_ACCESS_KEY_ID="${DB_USERNAME}"
AWS_SECRET_ACCESS_KEY="${DB_USERNAME}"
AWS_BUCKET=local
AWS_DEFAULT_REGION=eu-west-1

Navigate to any game in Filament and click the Media tab, ie: http://localhost:64000/manage/games/4874/media
Screenshot 2026-02-22 at 7 44 34 AM

In the In-Game Screenshot section, upload a file and click the Save Changes button:
Screenshot 2026-02-22 at 7 46 11 AM

Screenshot 2026-02-22 at 7 46 18 AM

You should now be able to verify:

  1. A game_screenshots row is created with is_primary = 1 and status = approved.
  2. A media row is created with collection_name = screenshots and custom_properties containing sha1 and legacy_path.
  3. The game's image_ingame_asset_path column is updated to the new /Images/NNNNNN.png path.
  4. In Minio, the original + sm/md/lg WebP & AVIF conversions are present under local/game/screenshots. Note, an LG image is only created if the source image is large enough to support it - we never scale up, only down.

Remember, Minio is accessed at http://localhost:64041/buckets and the credentials are retroachievements / retroachievements.

Screenshot 2026-02-22 at 7 48 39 AM

Visit the game page for the game. The ingame screenshot should render correctly (everything else being broken is expected given our env vars changes):
Screenshot 2026-02-22 at 7 49 18 AM

Upload a second in-game screenshot for the same game. Verify the first screenshot is demoted via is_primary = 0 and status = pending. The game page should now show the new screenshot:
Screenshot 2026-02-22 at 7 50 26 AM

What's next

This is the first of several phases for this work.

Tentatively, I envision the following phases looking like this:

The second phase will be for backfilling all existing legacy screenshots into the new system, migrating the game title screenshots to Media Library, and adding gallery management in Filament (view existing screenshots, set screenshots to primary, delete stuff, reorder stuff).

The third phase will be for viewing multiple ingame screenshots on the game page.

The fourth phase will be for supporting player uploads, which go into a Filament-powered review queue (really need to think through this).

@wescopeland wescopeland requested a review from a team February 22, 2026 12:53
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, most of my time working on this PR was spent here in this migration.

I've done a ton of research and referenced many different sources. Still, as much as I've tried, I'm sure I've made a mistake somewhere. However, given this lives in the DB, we can always change these configuration settings without needing to do a code deploy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant