// Package res hittar och läser resurser (sprites, ljud, text, speldata). // // Resursmappen ligger normalt bredvid binären (`/resources`). Vid // utveckling (`go run`, dlv) ligger binären i en temp-mapp, så vi faller // tillbaka på arbetskatalogen och letar uppåt efter repots rot (go.mod). // Miljövariabeln JP_RESOURCES vinner alltid. package res import ( "encoding/json" "errors" "fmt" "image" _ "image/png" // registrerar PNG-avkodaren för image.Decode "os" "path/filepath" "sync" ) // EnvVar är miljövariabeln som pekar ut resursmappen explicit. const EnvVar = "JP_RESOURCES" // DirName är namnet på resursmappen bredvid binären / i repots rot. const DirName = "resources" var ( dirOnce sync.Once dirPath string dirErr error ) // Dir returnerar den absoluta sökvägen till resursmappen. Resultatet cachas. func Dir() (string, error) { dirOnce.Do(func() { exe, _ := os.Executable() cwd, _ := os.Getwd() dirPath, dirErr = Locate(os.Getenv(EnvVar), filepath.Dir(exe), cwd, dirExists) }) return dirPath, dirErr } // Locate är den rena uppslagslogiken bakom Dir, bruten ut för att kunna testas. // Ordning: env-variabel, mappen bredvid binären, arbetskatalogen och dess // föräldrar (stannar vid första som innehåller en resursmapp). func Locate(env, exeDir, cwd string, exists func(string) bool) (string, error) { if env != "" { if exists(env) { return filepath.Clean(env), nil } return "", fmt.Errorf("res: %s=%q pekar inte på en mapp", EnvVar, env) } var tried []string if exeDir != "" { c := filepath.Join(exeDir, DirName) if exists(c) { return c, nil } tried = append(tried, c) } for dir := cwd; dir != ""; { c := filepath.Join(dir, DirName) if exists(c) { return c, nil } tried = append(tried, c) parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return "", fmt.Errorf("res: hittade ingen %q-mapp (sökte: %v); sätt %s", DirName, tried, EnvVar) } func dirExists(p string) bool { st, err := os.Stat(p) return err == nil && st.IsDir() } // Path bygger en sökväg inuti resursmappen, t.ex. Path("sprites", "player.png"). func Path(parts ...string) (string, error) { d, err := Dir() if err != nil { return "", err } return filepath.Join(append([]string{d}, parts...)...), nil } // ReadFile läser en fil relativt resursmappen. func ReadFile(parts ...string) ([]byte, error) { p, err := Path(parts...) if err != nil { return nil, err } b, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("res: %w", err) } return b, nil } // LoadJSON läser och avkodar en JSON-fil relativt resursmappen. func LoadJSON(v any, parts ...string) error { b, err := ReadFile(parts...) if err != nil { return err } if err := json.Unmarshal(b, v); err != nil { return fmt.Errorf("res: %s: %w", filepath.Join(parts...), err) } return nil } // LoadImage läser en PNG relativt resursmappen och returnerar en image.Image. // (Konvertering till *ebiten.Image görs av anroparen så att detta paket // förblir fritt från Ebitengine och kan testas utan fönster.) func LoadImage(parts ...string) (image.Image, error) { p, err := Path(parts...) if err != nil { return nil, err } f, err := os.Open(p) if err != nil { return nil, fmt.Errorf("res: %w", err) } defer f.Close() img, _, err := image.Decode(f) if err != nil { return nil, fmt.Errorf("res: %s: %w", p, err) } return img, nil } // ErrNotFound kan användas av anropare för att skilja "saknas" från andra fel. var ErrNotFound = errors.New("res: resursen saknas")