Add modular capture backends, resolution profiles, Wayland support
This commit is contained in:
parent
3b363192f2
commit
80ba9b1b90
16 changed files with 2266 additions and 45 deletions
283
pkg/engine/resolution/profile.go
Normal file
283
pkg/engine/resolution/profile.go
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
// Package resolution provides a profile system for managing screen regions across different resolutions.
|
||||
//
|
||||
// Each game plugin can register resolution profiles that define named screen regions
|
||||
// for different resolutions. This allows the engine to work with games at various
|
||||
// resolutions without hardcoding pixel coordinates.
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Profile defines screen regions for a specific resolution.
|
||||
type Profile struct {
|
||||
Width int `yaml:"width"` // Screen width in pixels
|
||||
Height int `yaml:"height"` // Screen height in pixels
|
||||
Label string `yaml:"label"` // Human-readable label (e.g., "1080p", "720p")
|
||||
Regions map[string]image.Rectangle `yaml:"regions"` // Named screen regions
|
||||
}
|
||||
|
||||
// Registry holds profiles per game per resolution.
|
||||
type Registry struct {
|
||||
// profiles[gameID][resolution_key] = Profile
|
||||
profiles map[string]map[string]*Profile
|
||||
}
|
||||
|
||||
// NewRegistry creates a new resolution profile registry.
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
profiles: make(map[string]map[string]*Profile),
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds a resolution profile for a specific game.
|
||||
func (r *Registry) Register(gameID string, profile *Profile) error {
|
||||
if profile == nil {
|
||||
return fmt.Errorf("profile cannot be nil")
|
||||
}
|
||||
|
||||
if profile.Width <= 0 || profile.Height <= 0 {
|
||||
return fmt.Errorf("invalid resolution: %dx%d", profile.Width, profile.Height)
|
||||
}
|
||||
|
||||
if r.profiles[gameID] == nil {
|
||||
r.profiles[gameID] = make(map[string]*Profile)
|
||||
}
|
||||
|
||||
key := resolutionKey(profile.Width, profile.Height)
|
||||
r.profiles[gameID][key] = profile
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the profile for a game and resolution, or error if unsupported.
|
||||
func (r *Registry) Get(gameID string, width, height int) (*Profile, error) {
|
||||
gameProfiles, exists := r.profiles[gameID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no profiles registered for game %q", gameID)
|
||||
}
|
||||
|
||||
key := resolutionKey(width, height)
|
||||
profile, exists := gameProfiles[key]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no profile for game %q at resolution %dx%d", gameID, width, height)
|
||||
}
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// GetRegion returns a named region for a game and resolution.
|
||||
func (r *Registry) GetRegion(gameID string, width, height int, regionName string) (image.Rectangle, error) {
|
||||
profile, err := r.Get(gameID, width, height)
|
||||
if err != nil {
|
||||
return image.Rectangle{}, err
|
||||
}
|
||||
|
||||
region, exists := profile.Regions[regionName]
|
||||
if !exists {
|
||||
return image.Rectangle{}, fmt.Errorf("region %q not found in profile for %s at %dx%d", regionName, gameID, width, height)
|
||||
}
|
||||
|
||||
return region, nil
|
||||
}
|
||||
|
||||
// SupportedResolutions returns resolutions available for a game.
|
||||
func (r *Registry) SupportedResolutions(gameID string) []image.Point {
|
||||
gameProfiles, exists := r.profiles[gameID]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resolutions []image.Point
|
||||
for _, profile := range gameProfiles {
|
||||
resolutions = append(resolutions, image.Point{X: profile.Width, Y: profile.Height})
|
||||
}
|
||||
|
||||
// Sort by resolution (width first, then height)
|
||||
sort.Slice(resolutions, func(i, j int) bool {
|
||||
if resolutions[i].X != resolutions[j].X {
|
||||
return resolutions[i].X < resolutions[j].X
|
||||
}
|
||||
return resolutions[i].Y < resolutions[j].Y
|
||||
})
|
||||
|
||||
return resolutions
|
||||
}
|
||||
|
||||
// ListGames returns all game IDs that have registered profiles.
|
||||
func (r *Registry) ListGames() []string {
|
||||
var games []string
|
||||
for gameID := range r.profiles {
|
||||
games = append(games, gameID)
|
||||
}
|
||||
sort.Strings(games)
|
||||
return games
|
||||
}
|
||||
|
||||
// GetProfiles returns all profiles for a specific game.
|
||||
func (r *Registry) GetProfiles(gameID string) []*Profile {
|
||||
gameProfiles, exists := r.profiles[gameID]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
var profiles []*Profile
|
||||
for _, profile := range gameProfiles {
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
|
||||
// Sort by resolution
|
||||
sort.Slice(profiles, func(i, j int) bool {
|
||||
if profiles[i].Width != profiles[j].Width {
|
||||
return profiles[i].Width < profiles[j].Width
|
||||
}
|
||||
return profiles[i].Height < profiles[j].Height
|
||||
})
|
||||
|
||||
return profiles
|
||||
}
|
||||
|
||||
// ValidateProfile checks if a profile is valid and complete.
|
||||
func (r *Registry) ValidateProfile(profile *Profile) error {
|
||||
if profile == nil {
|
||||
return fmt.Errorf("profile is nil")
|
||||
}
|
||||
|
||||
if profile.Width <= 0 || profile.Height <= 0 {
|
||||
return fmt.Errorf("invalid resolution: %dx%d", profile.Width, profile.Height)
|
||||
}
|
||||
|
||||
if profile.Label == "" {
|
||||
return fmt.Errorf("profile label is required")
|
||||
}
|
||||
|
||||
if profile.Regions == nil {
|
||||
return fmt.Errorf("profile regions map is nil")
|
||||
}
|
||||
|
||||
// Validate that all regions are within screen bounds
|
||||
screenBounds := image.Rect(0, 0, profile.Width, profile.Height)
|
||||
for regionName, region := range profile.Regions {
|
||||
if !region.In(screenBounds) {
|
||||
return fmt.Errorf("region %q is outside screen bounds: %v not in %v", regionName, region, screenBounds)
|
||||
}
|
||||
|
||||
if region.Empty() {
|
||||
return fmt.Errorf("region %q is empty: %v", regionName, region)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterMultiple registers multiple profiles for a game.
|
||||
func (r *Registry) RegisterMultiple(gameID string, profiles []*Profile) error {
|
||||
for _, profile := range profiles {
|
||||
if err := r.ValidateProfile(profile); err != nil {
|
||||
return fmt.Errorf("invalid profile %s: %w", profile.Label, err)
|
||||
}
|
||||
|
||||
if err := r.Register(gameID, profile); err != nil {
|
||||
return fmt.Errorf("failed to register profile %s: %w", profile.Label, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of a profile.
|
||||
func (p *Profile) Clone() *Profile {
|
||||
clone := &Profile{
|
||||
Width: p.Width,
|
||||
Height: p.Height,
|
||||
Label: p.Label,
|
||||
Regions: make(map[string]image.Rectangle),
|
||||
}
|
||||
|
||||
for name, region := range p.Regions {
|
||||
clone.Regions[name] = region
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// HasRegion checks if a profile contains a named region.
|
||||
func (p *Profile) HasRegion(name string) bool {
|
||||
_, exists := p.Regions[name]
|
||||
return exists
|
||||
}
|
||||
|
||||
// ListRegions returns all region names in the profile.
|
||||
func (p *Profile) ListRegions() []string {
|
||||
var names []string
|
||||
for name := range p.Regions {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// AspectRatio returns the aspect ratio of the profile resolution.
|
||||
func (p *Profile) AspectRatio() float64 {
|
||||
return float64(p.Width) / float64(p.Height)
|
||||
}
|
||||
|
||||
// IsWidescreen returns true if the profile is widescreen (16:9 or wider).
|
||||
func (p *Profile) IsWidescreen() bool {
|
||||
return p.AspectRatio() >= 16.0/9.0
|
||||
}
|
||||
|
||||
// ScaleFrom creates a new profile by scaling regions from another resolution.
|
||||
func (p *Profile) ScaleFrom(source *Profile) *Profile {
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
scaleX := float64(p.Width) / float64(source.Width)
|
||||
scaleY := float64(p.Height) / float64(source.Height)
|
||||
|
||||
scaled := &Profile{
|
||||
Width: p.Width,
|
||||
Height: p.Height,
|
||||
Label: p.Label,
|
||||
Regions: make(map[string]image.Rectangle),
|
||||
}
|
||||
|
||||
for name, region := range source.Regions {
|
||||
scaled.Regions[name] = image.Rect(
|
||||
int(float64(region.Min.X)*scaleX),
|
||||
int(float64(region.Min.Y)*scaleY),
|
||||
int(float64(region.Max.X)*scaleX),
|
||||
int(float64(region.Max.Y)*scaleY),
|
||||
)
|
||||
}
|
||||
|
||||
return scaled
|
||||
}
|
||||
|
||||
// resolutionKey generates a unique key for a resolution.
|
||||
func resolutionKey(width, height int) string {
|
||||
return fmt.Sprintf("%dx%d", width, height)
|
||||
}
|
||||
|
||||
// ParseResolution parses a resolution string like "1920x1080".
|
||||
func ParseResolution(s string) (width, height int, err error) {
|
||||
n, err := fmt.Sscanf(s, "%dx%d", &width, &height)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid resolution format %q: %w", s, err)
|
||||
}
|
||||
if n != 2 {
|
||||
return 0, 0, fmt.Errorf("invalid resolution format %q: expected WIDTHxHEIGHT", s)
|
||||
}
|
||||
if width <= 0 || height <= 0 {
|
||||
return 0, 0, fmt.Errorf("invalid resolution: %dx%d", width, height)
|
||||
}
|
||||
return width, height, nil
|
||||
}
|
||||
|
||||
// FormatResolution formats width and height as a resolution string.
|
||||
func FormatResolution(width, height int) string {
|
||||
return fmt.Sprintf("%dx%d", width, height)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue