Skip to content
Open
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
20 changes: 16 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ jobs:
env:
# Optional code signing (docs/SIGNING.md). Without these the build is
# unsigned; everything else works the same. SignPath signs tagged
# builds; a .pfx (SIGN_PFX_*) signs every build.
# builds; a .pfx (SIGN_PFX_*) signs pushes to main and tags. Pull
# requests are never signed: Dependabot's and forks' runs have no
# secrets, and the job has to pass for them too.
SIGN_PFX_BASE64: ${{ secrets.SIGN_PFX_BASE64 }}
SIGN_PFX_PASSWORD: ${{ secrets.SIGN_PFX_PASSWORD }}
SIGN: ${{ github.event_name != 'pull_request' }}
SIGNPATH: ${{ secrets.SIGNPATH_API_TOKEN != '' && startsWith(github.ref, 'refs/tags/v') }}
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -71,6 +74,15 @@ jobs:
- name: Build
run: wails3 build VERSION=${{ env.WL_VERSION }}

- name: Vet
run: go vet ./...

- name: Lint
# Pinned like Wails; bump on purpose. The config is .golangci.yml.
uses: golangci/golangci-lint-action@v9
with:
version: v2.15.0

- name: Test
run: go test ./...

Expand Down Expand Up @@ -102,7 +114,7 @@ jobs:
}

- name: Sign
if: env.SIGNPATH != 'true'
if: env.SIGN == 'true' && env.SIGNPATH != 'true'
run: ./build/windows/sign.ps1 bin/Seaglass.exe

- name: WebView2 bootstrapper
Expand Down Expand Up @@ -148,12 +160,12 @@ jobs:
if ($env:SIGNPATH -eq 'true') {
Copy-Item signed-binaries/Seaglass.exe, signed-binaries/uninstall.exe bin/ -Force
$defs += "-DSIGNED_UNINSTALLER=$pwd\bin\uninstall.exe"
} elseif ($env:SIGN_PFX_BASE64) {
} elseif ($env:SIGN -eq 'true' -and $env:SIGN_PFX_BASE64) {
$defs += "-DSIGN_CMD=powershell -NoProfile -ExecutionPolicy Bypass -File $pwd\build\windows\sign.ps1"
}
& $env:MAKENSIS -V2 @defs build/windows/nsis/project.nsi
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if ($env:SIGNPATH -ne 'true') { ./build/windows/sign.ps1 bin/Seaglass-setup.exe }
if ($env:SIGN -eq 'true' -and $env:SIGNPATH -ne 'true') { ./build/windows/sign.ps1 bin/Seaglass-setup.exe }

- name: Upload installer for signing
id: unsigned-installer
Expand Down
35 changes: 35 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# golangci-lint (v2 config; the version CI runs is pinned in
# .github/workflows/build.yml). Locally: golangci-lint run ./internal/... ./tools/...
# (the root package needs frontend/dist from a build).
#
# errcheck and gosec are not on yet: the first run reported 189 and 259
# findings. They get turned on once those are worked through; see the
# follow-ups in the PR that added this file.
version: "2"

linters:
default: none
enable:
- govet
- ineffassign
- misspell
- staticcheck
- unused
settings:
staticcheck:
# golangci-lint's default set, minus ST1005: error strings here start
# with product names (Steam, Syncer, Epic, Seaglass) and are shown to
# the user as they are.
checks:
- all
- -ST1000
- -ST1003
- -ST1005
- -ST1016
- -ST1020
- -ST1021
- -ST1022

formatters:
enable:
- gofmt
2 changes: 1 addition & 1 deletion internal/achievements/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type List struct {
Partial bool `json:"-"`
}

// FixUplayINI: Seaglass can turn achievements on in the game's Uplay
// FixUplayINI marks that Seaglass can turn achievements on in the game's Uplay
// emulator ini (EnableUplay).
const FixUplayINI = "uplay-ini"

Expand Down
2 changes: 1 addition & 1 deletion internal/achievements/steamlocal.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func validLang(s string) bool {
return false
}
for _, r := range s {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r == '_') {
if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '_' {
return false
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/app/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"golang.org/x/sys/windows"
)

// Dev flags drive the real app from a test harness (tools/harness). They
// DevArgs are flags that drive the real app from a test harness (tools/harness). They
// only work in dev builds: a release build ignores them.
//
// --virtual-pad[=ps|xbox] plug in a virtual controller (SDL's own)
Expand Down
2 changes: 1 addition & 1 deletion internal/app/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestCrashCapture(t *testing.T) {
captureCrashes(os.Getenv("WL_CRASH_DIR"))
go func() { // outside the test's own recover
var m map[string]int
m["boom"] = 1 // nil map: an unhandled panic
m["boom"] = 1 //nolint:staticcheck // nil map: an unhandled panic, which is the point of this test
}()
// Wait for the crash to end the process. Nothing may signal back
// from the panicking goroutine: its deferred calls run while the
Expand Down
14 changes: 7 additions & 7 deletions internal/app/launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,13 @@ func TestGameForPath(t *testing.T) {
{ID: 5, Title: "Too broad", Installed: true, Dir: `C:\Program Files`},
}
for path, want := range map[string]int64{
`D:\Games\Hades\x64\Hades.exe`: 2, // the most specific folder
`D:\Games\Other\Other.exe`: 1,
`D:\Games\Gone\Gone.exe`: 1, // not installed: only the folder around it counts
`C:\Tools\Editor\editor.exe`: 0, // hidden
`C:\Program Files\Some\app.exe`: 0, // too broad a folder
`D:\Games\Hades\Seaglass.exe`: 0,
`C:\Windows\System32\notepad.exe`: 0,
`D:\Games\Hades\x64\Hades.exe`: 2, // the most specific folder
`D:\Games\Other\Other.exe`: 1,
`D:\Games\Gone\Gone.exe`: 1, // not installed: only the folder around it counts
`C:\Tools\Editor\editor.exe`: 0, // hidden
`C:\Program Files\Some\app.exe`: 0, // too broad a folder
`D:\Games\Hades\Seaglass.exe`: 0,
`C:\Windows\System32\notepad.exe`: 0,
} {
g, ok := gameForPath(games, path)
if got := map[bool]int64{true: g.ID, false: 0}[ok]; got != want {
Expand Down
2 changes: 1 addition & 1 deletion internal/app/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (w *metaWorker) queueMissing() {
now := time.Now()
w.mu.Lock()
for _, g := range games {
if !(g.Installed || showOwned && g.Owned) || g.Hidden || w.queued[g.ID] {
if !g.Installed && (!showOwned || !g.Owned) || g.Hidden || w.queued[g.ID] {
continue
}
if t, ok := w.failed[g.ID]; ok && now.Sub(t) < metaRetryGap {
Expand Down
1 change: 0 additions & 1 deletion internal/app/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ type profileState struct {
mu sync.Mutex
info Profile
unl map[string]map[string]int64 // merged achievements by game key
pcs string // PC the settings came from
data time.Time // last launcherData call that worked
kick chan struct{}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/emulated/cemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func isTitleID(s string) bool {
return false
}
for _, r := range strings.ToLower(s) {
if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f') {
if (r < '0' || r > '9') && (r < 'a' || r > 'f') {
return false
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/emulated/emulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ func isDiscID(s string) bool {
return false
}
for _, r := range s {
if !(r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') {
if (r < 'A' || r > 'Z') && (r < '0' || r > '9') {
return false
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/emulated/rpcs3.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var rpcs3 = &emulator{
return games, roots
},
folder: func(in *install, dir string, names map[string]bool) (Game, bool) {
if !names["ps3_game"] && !(names["param.sfo"] && names["usrdir"]) {
if !names["ps3_game"] && (!names["param.sfo"] || !names["usrdir"]) {
return Game{}, false
}
return ps3Game(dir)
Expand Down Expand Up @@ -118,7 +118,7 @@ func isSerial(s string) bool {
return false
}
for i, r := range s {
if i < 4 && !(r >= 'A' && r <= 'Z') || i >= 4 && !(r >= '0' && r <= '9') {
if i < 4 && (r < 'A' || r > 'Z') || i >= 4 && (r < '0' || r > '9') {
return false
}
}
Expand Down
3 changes: 1 addition & 2 deletions internal/launch/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ func (m *Manager) follow(ctx context.Context, p Plan, pid uint32, before map[uin
}
secs := int64(total)
m.update(func(s *Session) { s.Seconds = secs })
case now.Sub(began) > timeout && !(starting && now.Sub(began) <= starterTimeout):
case now.Sub(began) > timeout && (!starting || now.Sub(began) > starterTimeout):
m.update(func(s *Session) {
s.Phase = Ended
s.Note = "Seaglass couldn't see the game running, so no playtime was counted."
Expand All @@ -512,7 +512,6 @@ func (m *Manager) follow(ctx context.Context, p Plan, pid uint32, before map[uin
}
select {
case <-m.stop:
total += gap
unsaved += gap
flush()
return -1
Expand Down
2 changes: 1 addition & 1 deletion internal/meta/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ func PruneArt(artDir string, keep map[string]bool, grace time.Duration) (removed
cutoff := time.Now().Add(-grace)
for _, e := range entries {
name := e.Name()
if e.IsDir() || keep[artPrefix+name] || !(reArtName.MatchString(name) || strings.HasSuffix(name, ".tmp")) {
if e.IsDir() || keep[artPrefix+name] || (!reArtName.MatchString(name) && !strings.HasSuffix(name, ".tmp")) {
continue
}
fi, err := e.Info()
Expand Down
6 changes: 3 additions & 3 deletions internal/owned/achievements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,15 @@ func TestGOGCode(t *testing.T) {

func TestGOGAchievementsAPI(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/token":
switch r.URL.Path {
case "/token":
q := r.URL.Query()
if q.Get("client_id") != gogClientID || q.Get("grant_type") != "refresh_token" || q.Get("refresh_token") != "r1" {
w.WriteHeader(400)
return
}
w.Write([]byte(`{"access_token":"a2","refresh_token":"r2","user_id":"u1","expires_in":3600}`))
case r.URL.Path == "/clients/5134/users/u1/achievements":
case "/clients/5134/users/u1/achievements":
if r.Header.Get("Authorization") != "Bearer a2" {
w.WriteHeader(401)
return
Expand Down
6 changes: 3 additions & 3 deletions internal/platform/foreground.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var (
procTranslateMessage = user32.NewProc("TranslateMessage")
procDispatchMessageW = user32.NewProc("DispatchMessageW")
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
procGetWindowThreadProcessID = user32.NewProc("GetWindowThreadProcessId")
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
)

Expand All @@ -30,7 +30,7 @@ func ForegroundPID() uint32 {
return 0
}
var pid uint32
procGetWindowThreadProcessId.Call(h, uintptr(unsafe.Pointer(&pid)))
procGetWindowThreadProcessID.Call(h, uintptr(unsafe.Pointer(&pid)))
return pid
}

Expand All @@ -51,7 +51,7 @@ var (

func foregroundCallback(hook, event, hwnd, idObject, idChild, thread, when uintptr) uintptr {
var pid uint32
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
procGetWindowThreadProcessID.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
fgMu.Lock()
fn := fgFn
fgMu.Unlock()
Expand Down
4 changes: 2 additions & 2 deletions internal/platform/signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ func TestSigner(t *testing.T) {
func TestCommandExe(t *testing.T) {
for in, want := range map[string]string{
`"C:\Program Files\Seaglass\Seaglass.exe" --tray`: `C:\Program Files\Seaglass\Seaglass.exe`,
`C:\WL\Seaglass.exe --tray`: `C:\WL\Seaglass.exe`,
`"unterminated`: ``,
`C:\WL\Seaglass.exe --tray`: `C:\WL\Seaglass.exe`,
`"unterminated`: ``,
} {
if got := commandExe(in); got != want {
t.Errorf("commandExe(%q) = %q, want %q", in, got, want)
Expand Down
2 changes: 1 addition & 1 deletion internal/platform/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func BringToFront(hwnd uintptr) bool {
if fg == 0 {
return false
}
fgThread, _, _ := procGetWindowThreadProcessId.Call(fg, 0)
fgThread, _, _ := procGetWindowThreadProcessID.Call(fg, 0)
self := uintptr(windows.GetCurrentThreadId())
if fgThread != 0 && fgThread != self {
procAttachThreadInput.Call(self, fgThread, 1)
Expand Down
2 changes: 1 addition & 1 deletion internal/scan/steam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestSteamCandidatesOldLibraryFormat(t *testing.T) {
"steamapps/libraryfolders.vdf": `"LibraryFolders" { "TimeNextStatsReport" "1" "1" "` + vdfPath + `" }`,
})
mk(t, lib, map[string]string{
"steamapps/appmanifest_413150.acf": `"AppState" { "appid" "413150" "name" "Stardew Valley" "StateFlags" "4" "installdir" "Stardew Valley" }`,
"steamapps/appmanifest_413150.acf": `"AppState" { "appid" "413150" "name" "Stardew Valley" "StateFlags" "4" "installdir" "Stardew Valley" }`,
"steamapps/common/Stardew Valley/Stardew Valley.exe": "x",
})
libs := SteamLibraries(root)
Expand Down
4 changes: 2 additions & 2 deletions internal/syncer/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,10 @@ func checkServer(conn net.Conn) error {
return nil
}

var procGetNamedPipeServerProcessId = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetNamedPipeServerProcessId")
var procGetNamedPipeServerProcessID = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetNamedPipeServerProcessId")

func getNamedPipeServerProcessID(h windows.Handle, pid *uint32) error {
r, _, err := procGetNamedPipeServerProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(pid)))
r, _, err := procGetNamedPipeServerProcessID.Call(uintptr(h), uintptr(unsafe.Pointer(pid)))
if r == 0 {
return err
}
Expand Down
4 changes: 1 addition & 3 deletions internal/update/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ func (f *fakeGitHub) publish(tag string, files map[string][]byte, extra ...map[s
f.files[name] = b
assets = append(assets, map[string]any{"name": name, "size": len(b), "browser_download_url": f.srv.URL + "/download/" + name})
}
for _, e := range extra {
assets = append(assets, e)
}
assets = append(assets, extra...)
f.release = map[string]any{"tag_name": tag, "body": "notes", "html_url": "https://github.com/ApolloF/Seaglass/releases/tag/" + tag, "assets": assets}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/vpad/vigem.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func NewX360() (*X360, error) {
if err != nil || len(paths) == 0 {
return nil, ErrNoViGEm
}
var last error = ErrNoViGEm
last := ErrNoViGEm
for _, p := range paths {
h, err := openBus(p)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func main() {
platform.WaitInstanceGone(oldUniqueID, 15*time.Second)
}
var moved []string
if !(dev && args.Dev.DataDir != "") && !platform.InstanceRunning(uniqueID) {
if (!dev || args.Dev.DataDir == "") && !platform.InstanceRunning(uniqueID) {
// WaterLauncher's library and settings.
moved = platform.MoveOldData(platform.InstanceRunning(oldUniqueID))
}
Expand Down
20 changes: 10 additions & 10 deletions tools/release/cut.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ import (
// cut releases a version from main, one checked step at a time; the first
// failing check stops it before anything that can't be undone.
//
// go run ./tools/release cut v1.2.0 [--pr 12] [--dry-run]
// go run ./tools/release cut v1.2.0 [--pr 12] [--dry-run]
//
// 1. This PC's release key is one the updater trusts.
// 2. The tag is a version, its notes exist, it isn't used yet.
// 3. The working tree is clean, on main, level with origin/main.
// 4. With --pr: that pull request is merged and its merge commit is in main.
// 5. CI passed for main's head (waits while it runs).
// 6. "Version X.Y.Z" (build/windows/info.json) goes into main through a
// pull request, since main takes nothing else; skipped if already there.
// 7. The tag is pushed; CI builds and tests it and makes a draft release.
// 8. publish signs the draft with the release key and makes it public.
// 1. This PC's release key is one the updater trusts.
// 2. The tag is a version, its notes exist, it isn't used yet.
// 3. The working tree is clean, on main, level with origin/main.
// 4. With --pr: that pull request is merged and its merge commit is in main.
// 5. CI passed for main's head (waits while it runs).
// 6. "Version X.Y.Z" (build/windows/info.json) goes into main through a
// pull request, since main takes nothing else; skipped if already there.
// 7. The tag is pushed; CI builds and tests it and makes a draft release.
// 8. publish signs the draft with the release key and makes it public.
func cut(tag string, pr string, dry bool) error {
num, err := versionNumber(tag)
if err != nil {
Expand Down