diff --git a/README.md b/README.md index e2f3c1b..a354208 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Y = hjälp, X = ordlista, Back/Select = karta. | `go run ./tools/build …` | bygger (se ovan) | | `go run ./tools/genart` | genererar placeholder-grafiken `resources/sprites/tiles.png` och `player.png` | | `go run ./tools/gensound` | genererar placeholder-ljuden `resources/sound/*.wav` (avgångsmelodi, dörrsignal) | -| `go run ./tools/shots` | tar dokumentationens skärmdumpar till `doc/img/` – **kör efter varje grafik-/UI-ändring och committa bilderna** | +| `go run ./tools/shots` | tar dokumentationens skärmdumpar till `doc/img/` – **kör efter varje grafik-/UI-ändring och committa bilderna**. På Linux körs spelet på en osynlig skärm (headless Wayfire + Xwayland) så att inget fönster syns eller tar fokus; `METRO_HEADLESS=0` använder skärmen | ## Beroenden diff --git a/doc/img/items.png b/doc/img/items.png index 37e4466..a219c8e 100644 Binary files a/doc/img/items.png and b/doc/img/items.png differ diff --git a/doc/img/map.png b/doc/img/map.png index 37e4466..dbb80bf 100644 Binary files a/doc/img/map.png and b/doc/img/map.png differ diff --git a/doc/img/result.png b/doc/img/result.png index 16a17ce..6c89658 100644 Binary files a/doc/img/result.png and b/doc/img/result.png differ diff --git a/doc/img/ride.png b/doc/img/ride.png index 39e4f6d..c0e1fa7 100644 Binary files a/doc/img/ride.png and b/doc/img/ride.png differ diff --git a/doc/img/skills.png b/doc/img/skills.png index 91be59d..33ad79e 100644 Binary files a/doc/img/skills.png and b/doc/img/skills.png differ diff --git a/doc/img/ticket.png b/doc/img/ticket.png index 27231f4..eb8bb88 100644 Binary files a/doc/img/ticket.png and b/doc/img/ticket.png differ diff --git a/doc/img/words.png b/doc/img/words.png index 37e4466..3889a13 100644 Binary files a/doc/img/words.png and b/doc/img/words.png differ diff --git a/doc/roadmap.md b/doc/roadmap.md index b2b3c7a..cb3aaf2 100644 --- a/doc/roadmap.md +++ b/doc/roadmap.md @@ -107,6 +107,15 @@ go run ./tools/shots # bygger debug, kör spelet med -script, skriver doc/img committa `doc/img/` i samma commit, så visar README och wiki alltid aktuell grafik. Nya vyer läggs till i listan i `tools/shots/main.go`. +**Osynlig skärm:** på Linux startar `tools/shots` en egen headless +wlroots-kompositor (Wayfire med `WLR_BACKENDS=headless` och Xwayland) och +kör spelet där, så att skärmdumparna varken syns eller tar fokus från det +man själv gör på datorn. Bilderna är pixelidentiska med riktiga skärmen +(mjukvarurendering, nearest-filter). `METRO_HEADLESS=0` stänger av det; +saknas `wayfire` används skärmen. Samma knep för manuella körningar: +`DISPLAY=:N WAYLAND_DISPLAY=wayland-N ./metro-puzzle-jp …` mot en startad +headless-instans. + ### CI `.gitea/workflows/release.yaml` kör `go vet` + `go test`, bygger diff --git a/internal/game/script.go b/internal/game/script.go index 9d69eef..f3aa2ad 100644 --- a/internal/game/script.go +++ b/internal/game/script.go @@ -206,6 +206,7 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error { s.advance() } case "shot": + g.scene.Draw(g, frame) // rita nu, så att bilden speglar exakt denna uppdatering oavsett FPS if err := saveShot(frame, st.arg); err != nil { return err } diff --git a/tools/shots/headless.go b/tools/shots/headless.go new file mode 100644 index 0000000..457c4f1 --- /dev/null +++ b/tools/shots/headless.go @@ -0,0 +1,128 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "syscall" + "time" +) + +// Headless skärm för skärmdumparna (Linux): en egen wlroots-kompositor +// (Wayfire) med headless-backend och Xwayland, så att spelfönstren varken +// syns eller tar fokus från det Björn gör på datorn. Finns inte wayfire, +// eller sätts METRO_HEADLESS=0, används den vanliga skärmen. + +type headless struct { + cmd *exec.Cmd + wayland string // t.ex. wayland-1 + x11 string // t.ex. :1 +} + +var ( + reSocket = regexp.MustCompile(`Using socket name (wayland-\d+)`) + reXwayland = regexp.MustCompile(`Starting Xwayland on (:\d+)`) +) + +// startHeadless startar kompositorn och väntar på socket + Xwayland. +// Returnerar nil (utan fel) när headless inte är tillgängligt. +func startHeadless(buildDir string) (*headless, error) { + if runtime.GOOS != "linux" || os.Getenv("METRO_HEADLESS") == "0" { + return nil, nil + } + bin, err := exec.LookPath("wayfire") + if err != nil { + return nil, nil + } + ini := filepath.Join(buildDir, "wayfire-headless.ini") + if err := os.WriteFile(ini, []byte("[core]\nplugins = \nxwayland = true\npreferred_decoration_mode = server\nvwidth = 1\nvheight = 1\n"), 0o644); err != nil { + return nil, err + } + cmd := exec.Command(bin, "-c", ini) + cmd.Env = append(os.Environ(), "WLR_BACKENDS=headless", "WLR_LIBINPUT_NO_DEVICES=1", "WLR_RENDERER=pixman") + logf, err := os.Create(filepath.Join(buildDir, "wayfire-headless.log")) + if err != nil { + return nil, err + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + cmd.Stdout, cmd.Stderr = pw, pw + if err := cmd.Start(); err != nil { + return nil, err + } + pw.Close() + h := &headless{cmd: cmd} + done := make(chan struct{}) + go func() { + sc := bufio.NewScanner(pr) + for sc.Scan() { + line := sc.Text() + fmt.Fprintln(logf, line) + if m := reSocket.FindStringSubmatch(line); m != nil { + h.wayland = m[1] + } + if m := reXwayland.FindStringSubmatch(line); m != nil { + h.x11 = m[1] + } + if h.wayland != "" && h.x11 != "" { + select { + case <-done: + default: + close(done) + } + } + } + }() + select { + case <-done: + case <-time.After(10 * time.Second): + h.stop() + return nil, fmt.Errorf("headless-kompositorn startade inte inom 10 s (se %s)", logf.Name()) + } + time.Sleep(500 * time.Millisecond) // låt Xwayland bli redo + return h, nil +} + +// env ger miljön som pekar spelet mot den osynliga skärmen. +func (h *headless) env() []string { + var out []string + for _, e := range os.Environ() { + if strings.HasPrefix(e, "DISPLAY=") || strings.HasPrefix(e, "WAYLAND_DISPLAY=") { + continue + } + out = append(out, e) + } + return append(out, "DISPLAY="+h.x11, "WAYLAND_DISPLAY="+h.wayland) +} + +// stop stänger kompositorn snällt (SIGTERM, så att den tar bort sina +// sockets) och städar det som ändå blir kvar. +func (h *headless) stop() { + if h == nil || h.cmd == nil || h.cmd.Process == nil { + return + } + _ = h.cmd.Process.Signal(syscall.SIGTERM) + done := make(chan struct{}) + go func() { _, _ = h.cmd.Process.Wait(); close(done) }() + select { + case <-done: + case <-time.After(3 * time.Second): + _ = h.cmd.Process.Kill() + <-done + } + if rt := os.Getenv("XDG_RUNTIME_DIR"); rt != "" && h.wayland != "" { + os.Remove(filepath.Join(rt, h.wayland)) + os.Remove(filepath.Join(rt, h.wayland+".lock")) + } + if h.x11 != "" { + os.Remove(filepath.Join("/tmp/.X11-unix", "X"+strings.TrimPrefix(h.x11, ":"))) + } + h.cmd = nil +} diff --git a/tools/shots/main.go b/tools/shots/main.go index d3d6d35..79656b2 100644 --- a/tools/shots/main.go +++ b/tools/shots/main.go @@ -6,6 +6,10 @@ // PNG:er till doc/img/. Vägarna genom banorna räknas ut med BFS från // bandatan. Kör om detta när grafik, UI eller banor ändrats, och committa // bilderna – README och wikin pekar på dem. +// +// På Linux körs spelet på en osynlig skärm (headless Wayfire + Xwayland, +// se headless.go) så att inget fönster syns eller tar fokus. METRO_HEADLESS=0 +// stänger av det. package main import ( @@ -164,6 +168,14 @@ func main() { if runtime.GOOS == "windows" { exe += ".exe" } + hl, err := startHeadless(filepath.Join(root, "build")) + must(err) + if hl != nil { + fmt.Printf("» osynlig skärm: %s / %s (inget syns på skrivbordet)\n", hl.wayland, hl.x11) + defer hl.stop() + } else { + fmt.Println("» ingen headless-kompositor (wayfire saknas eller METRO_HEADLESS=0): använder skärmen") + } _ = demo for _, s := range shots { args := []string{"-mute", "-nosave", "-script", strings.ReplaceAll(s.script, "$OUT", out)} @@ -175,7 +187,11 @@ func main() { } run := exec.Command(exe, args...) run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr + if hl != nil { + run.Env = hl.env() + } if err := run.Run(); err != nil { + hl.stop() fail(fmt.Errorf("%s: %w", s.script, err)) } }