Files
metro-puzzle-jp/internal/res/res.go
claude c1f9546af6 M0: skelett, byggkedja och dokumentation för JP Subway Pussel
- Go 1.27 + Ebitengine v2.10, intern upplösning 320x180 med heltalsskalning
- internal/res (resursuppslag), internal/rng (seedad slump), internal/level
  (banformat, validering, BFS-lösbarhet) med tester; internal/archtest
  kontrollerar att bara presentationslagret beror på Ebitengine
- tools/build: debug -> build/debug/, release -> build/release/<os>-<arch>/
  med resources/ bredvid binären; Windows korsbyggs utan C-verktyg
- VS Code tasks (build/test/release) och launch (dlv)
- Första banan 001-flygplatsen, sv/en-texter, skyltordbok, placeholder-sprite
- doc/: spelbeskrivning (renskriven), mål och tester, roadmap med teknikval
  (M0–M12), plan.md; README med bygg- och beroendeinformation

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 18:52:10 +02:00

136 lines
3.5 KiB
Go

// Package res hittar och läser resurser (sprites, ljud, text, speldata).
//
// Resursmappen ligger normalt bredvid binären (`<exe-mapp>/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")