Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/opensave-app/frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@
"home.trackGameHint": "Find a game by name, or choose its program or save folder",
"home.addGame.recognisedAs": "Recognised as \"{name}\" — its cover art comes from the game database.",
"home.findGame.title": "Track a game from the game database",
"home.findGame.runningNow": "Running now",
"home.findGame.lookingAtRunning": "Looking at what is running…",
"home.findGame.nothingRunning": "No game the game database knows is running right now.",
"home.findGame.findByName": "Find by name",
"home.findGame.searchPlaceholder": "e.g. Crimson Desert",
"home.findGame.searching": "Searching…",
Expand Down
3 changes: 3 additions & 0 deletions cmd/opensave-app/frontend/src/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@
"home.trackGameHint": "按名称查找游戏,或选择其程序或存档文件夹",
"home.addGame.recognisedAs": "已识别为“{name}”——封面来自游戏数据库。",
"home.findGame.title": "从游戏数据库跟踪游戏",
"home.findGame.runningNow": "正在运行",
"home.findGame.lookingAtRunning": "正在查看正在运行的程序…",
"home.findGame.nothingRunning": "当前没有游戏数据库收录的游戏在运行。",
"home.findGame.findByName": "按名称查找",
"home.findGame.searchPlaceholder": "例如:Crimson Desert",
"home.findGame.searching": "搜索中…",
Expand Down
3 changes: 3 additions & 0 deletions cmd/opensave-app/frontend/src/locales/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@
"home.trackGameHint": "按名稱查詢遊戲,或選擇其程式或存檔資料夾",
"home.addGame.recognisedAs": "已識別為“{name}”——封面來自遊戲資料庫。",
"home.findGame.title": "從遊戲資料庫跟蹤遊戲",
"home.findGame.runningNow": "正在執行",
"home.findGame.lookingAtRunning": "正在查看正在執行的程式…",
"home.findGame.nothingRunning": "目前沒有遊戲資料庫收錄的遊戲在執行。",
"home.findGame.findByName": "按名稱查詢",
"home.findGame.searchPlaceholder": "例如:Crimson Desert",
"home.findGame.searching": "搜尋中…",
Expand Down
35 changes: 33 additions & 2 deletions cmd/opensave-app/frontend/src/views/home/TrackGameDialog.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script>
// Tracking a game by hand, as the game database knows it: one found by
// name, or the one a program or folder belongs to. Whatever it is picked by,
// Tracking a game by hand, as the game database knows it: one running now,
// one found by name, or the one a program or folder belongs to. Whatever it
// is picked by,
// it is tracked with its name, App ID (cover art, launching) and a save
// location the database knows on this device — what a folder tracked on its
// own lacked. A game tracked already under another entry is offered to link
Expand All @@ -23,6 +24,17 @@
let searching = false;
let note = '';

// The games running now that the game database knows: the game being
// played is the one most often tracked. null while being looked for.
let running = null;
(async () => {
try {
running = (await api.get('/api/gamedb/running')).games ?? [];
} catch {
running = [];
}
})();

// The game picked, and how it is to be tracked.
let picked = null;
let name = '';
Expand Down Expand Up @@ -137,6 +149,25 @@
<Modal title={$t('home.findGame.title')} icon={Gamepad2} width={620} height="auto" onClose={() => dispatch('close')}>
<div class="body">
{#if !picked}
<div class="field">
<span class="label">{$t('home.findGame.runningNow')}</span>
{#if running === null}
<p class="hint"><Spinner size={13} /> {$t('home.findGame.lookingAtRunning')}</p>
{:else if running.length === 0}
<p class="hint">{$t('home.findGame.nothingRunning')}</p>
{:else}
<div class="list">
{#each running as g (g.exe)}
<button class="row" on:click={() => choose(g, g.exe)}>
<span class="row-name">{g.name}</span>
<span class="row-sub">
{g.trackedName ? $t('home.findGame.trackedAs', { name: g.trackedName }) : g.exe}
</span>
</button>
{/each}
</div>
{/if}
</div>
<div class="field">
<label for="tg-search">{$t('home.findGame.findByName')}</label>
<input id="tg-search" placeholder={$t('home.findGame.searchPlaceholder')} bind:value={query} />
Expand Down
1 change: 1 addition & 0 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (s *Server) routes(r chi.Router) {
// The game database, for tracking a game by hand (daemon/gamedb.go).
r.Get("/api/gamedb/search", s.handleGameDBSearch)
r.Get("/api/gamedb/identify", s.handleGameDBIdentify)
r.Get("/api/gamedb/running", s.handleGameDBRunning)
r.Get("/api/suggest-name", s.handleSuggestName)
r.Post("/api/games/untrack-bulk", s.handleBulkUntrack)

Expand Down
6 changes: 6 additions & 0 deletions internal/api/routes_gamedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ func (s *Server) handleGameDBIdentify(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, http.StatusOK, map[string]any{"found": true, "game": m})
}

// GET /api/gamedb/running — the games the game database knows with a program
// running now.
func (s *Server) handleGameDBRunning(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"games": s.Daemon.RunningGames()})
}
44 changes: 44 additions & 0 deletions internal/daemon/gamedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package daemon
import (
"os"
"path/filepath"
"sort"
"strings"

"github.com/opensave/opensave/internal/presets"
Expand Down Expand Up @@ -88,3 +89,46 @@ func (d *Daemon) markTracked(m *presets.GameMatch) {
}
}
}

// RunningGame is a game the game database knows with a program of it running
// now: what "Track game" offers first, since the game being played is the one
// most often tracked.
type RunningGame struct {
presets.GameMatch
// Exe is the program seen running, tracked as the game's program.
Exe string `json:"exe"`
}

// RunningGames lists the games the game database knows that have a program
// running now, by where the program is installed (GameDB.IdentifyProgram);
// once per game, by name. Programs of the system itself are not looked at.
func (d *Daemon) RunningGames() []RunningGame {
if d.sessions.list == nil {
return nil
}
procs, err := d.sessions.list()
if err != nil {
return nil
}
db := d.gameDB()
winDir := strings.ToLower(os.Getenv("WINDIR"))
seen := map[string]bool{}
out := []RunningGame{}
for _, p := range procs {
if p.Exe == "" {
continue
}
if winDir != "" && strings.HasPrefix(strings.ToLower(p.Exe), winDir+string(filepath.Separator)) {
continue
}
m, ok := db.IdentifyProgram(p.Exe)
if !ok || seen[m.Name] {
continue
}
seen[m.Name] = true
d.markTracked(&m)
out = append(out, RunningGame{GameMatch: m, Exe: p.Exe})
}
sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name) })
return out
}
53 changes: 53 additions & 0 deletions internal/daemon/running_games_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package daemon

import (
"os"
"path/filepath"
"runtime"
"testing"

"github.com/opensave/opensave/internal/presets"
"github.com/opensave/opensave/internal/sessions"
)

// "Track game" offers first the games running now that the game database
// knows: once each, however many of their programs run, and never a program
// of the system's own.
func TestRunningGames_TheGamesTheDatabaseKnowsRunningNow(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("the game database below uses Windows paths")
}
d := newTestDaemon(t)
dir := t.TempDir()
manifest := `
Pearl Game:
files:
"<winLocalAppData>/Pearl/CD/save":
tags: [save]
installDir:
Pearl Game: {}
steam:
id: 3321460
`
if err := os.WriteFile(filepath.Join(dir, "ludusavi-manifest.yaml"), []byte(manifest), 0o666); err != nil {
t.Fatal(err)
}
d.Scanner = &presets.Scanner{CacheFile: filepath.Join(dir, "steam-app-cache.json"), GOOS: "windows"}
t.Setenv("WINDIR", `C:\Windows`)
d.sessions.list = func() ([]sessions.Proc, error) {
return []sessions.Proc{
{PID: 10, Exe: `G:\Games\Pearl Game\bin64\game.exe`},
{PID: 11, Exe: `G:\Games\Pearl Game\bin64\crashreporter.exe`},
{PID: 12, Exe: `C:\Windows\System32\svchost.exe`},
{PID: 13, Exe: `C:\Tools\editor.exe`},
}, nil
}

got := d.RunningGames()
if len(got) != 1 {
t.Fatalf("running games = %+v, want Pearl Game once", got)
}
if got[0].Name != "Pearl Game" || got[0].AppID != "3321460" || got[0].Exe != `G:\Games\Pearl Game\bin64\game.exe` {
t.Errorf("running game = %+v", got[0])
}
}
Loading