283 lines
No EOL
7.7 KiB
Go
283 lines
No EOL
7.7 KiB
Go
// 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)
|
|
} |