Skip to content

Commit

Permalink
feat: Remove Call of Duty running files before launch
Browse files Browse the repository at this point in the history
  • Loading branch information
cetteup committed Feb 2, 2023
1 parent a52681a commit fa05320
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 0 deletions.
12 changes: 12 additions & 0 deletions internal/titles/cod.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ var Cod = domain.GameTitle{
When: game_launcher.HookWhenPreLaunch,
ExitOnError: true,
},
{
Handler: internal.HookDeleteFile,
When: game_launcher.HookWhenPreLaunch,
ExitOnError: false,
},
},
},
URLValidator: internal.IPPortURLValidator{},
CmdBuilder: internal.MakeSimpleCmdBuilder(internal.PlusConnectPrefix),
HookHandlers: []game_launcher.HookHandler{
internal.MakeKillProcessHookHandler(true),
internal.MakeDeleteFileHookHandler(internal.CoDRunningFilePathsBuilder),
},
}

Expand All @@ -54,11 +60,17 @@ var CodUO = domain.GameTitle{
When: game_launcher.HookWhenPreLaunch,
ExitOnError: true,
},
{
Handler: internal.HookDeleteFile,
When: game_launcher.HookWhenPreLaunch,
ExitOnError: false,
},
},
},
URLValidator: internal.IPPortURLValidator{},
CmdBuilder: internal.MakeSimpleCmdBuilder(internal.PlusConnectPrefix),
HookHandlers: []game_launcher.HookHandler{
internal.MakeKillProcessHookHandler(true),
internal.MakeDeleteFileHookHandler(internal.CoDRunningFilePathsBuilder),
},
}
6 changes: 6 additions & 0 deletions internal/titles/cod2.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ var Cod2 = domain.GameTitle{
When: game_launcher.HookWhenPreLaunch,
ExitOnError: true,
},
{
Handler: internal.HookDeleteFile,
When: game_launcher.HookWhenPreLaunch,
ExitOnError: false,
},
},
},
URLValidator: internal.IPPortURLValidator{},
CmdBuilder: internal.MakeSimpleCmdBuilder(internal.PlusConnectPrefix),
HookHandlers: []game_launcher.HookHandler{
internal.MakeKillProcessHookHandler(true),
internal.MakeDeleteFileHookHandler(internal.CoDRunningFilePathsBuilder),
},
}
6 changes: 6 additions & 0 deletions internal/titles/cod4.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ var Cod4 = domain.GameTitle{
When: game_launcher.HookWhenPreLaunch,
ExitOnError: true,
},
{
Handler: internal.HookDeleteFile,
When: game_launcher.HookWhenPreLaunch,
ExitOnError: false,
},
},
},
URLValidator: internal.IPPortURLValidator{},
CmdBuilder: internal.MakeSimpleCmdBuilder(internal.PlusConnectPrefix),
HookHandlers: []game_launcher.HookHandler{
internal.MakeKillProcessHookHandler(true),
internal.MakeDeleteFileHookHandler(internal.CoDRunningFilePathsBuilder),
},
}
19 changes: 19 additions & 0 deletions internal/titles/codwaw.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package titles

import (
"fmt"
"path/filepath"
"strings"

"github.com/cetteup/joinme.click-launcher/internal/domain"
"github.com/cetteup/joinme.click-launcher/internal/titles/internal"
"github.com/cetteup/joinme.click-launcher/pkg/game_launcher"
Expand All @@ -26,11 +30,26 @@ var CodWaw = domain.GameTitle{
When: game_launcher.HookWhenPreLaunch,
ExitOnError: true,
},
{
Handler: internal.HookDeleteFile,
When: game_launcher.HookWhenPreLaunch,
ExitOnError: false,
},
},
},
URLValidator: internal.IPPortURLValidator{},
CmdBuilder: internal.MakeSimpleCmdBuilder(internal.PlusConnectPrefix),
HookHandlers: []game_launcher.HookHandler{
internal.MakeKillProcessHookHandler(true),
internal.MakeDeleteFileHookHandler(codWawRunningFilePathsBuilder),
},
}

var codWawRunningFilePathsBuilder = func(config game_launcher.Config) ([]string, error) {
name := fmt.Sprintf("__%s", strings.TrimSuffix(config.ExecutableName, filepath.Ext(config.ExecutableName)))
appData, err := internal.GetLocalAppDataPath()
if err != nil {
return nil, err
}
return []string{filepath.Join(appData, "Activision", "CoDWaW", name)}, nil
}
52 changes: 52 additions & 0 deletions internal/titles/internal/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"net"
"net/url"
"path/filepath"
"regexp"
"strings"

"github.com/mitchellh/go-ps"
"github.com/rs/zerolog/log"
Expand All @@ -15,6 +17,7 @@ import (

const (
HookKillProcess = "kill-process"
HookDeleteFile = "delete-running-file" // CoD games write a dummy file when launched. If the file is still present when launched again, the game assumes it crashed and offers to start in safe mode.
PlusConnectPrefix = "+connect"
Frostbite3GameIdPattern = `^\d+$` // game ids vary by length, so for now we are just validating that it only contains numbers
)
Expand Down Expand Up @@ -173,3 +176,52 @@ func (h KillProcessHookHandler) Run(fr game_launcher.FileRepository, u *url.URL,
func (h KillProcessHookHandler) String() string {
return HookKillProcess
}

func MakeDeleteFileHookHandler(pathsBuilder func(config game_launcher.Config) ([]string, error)) DeleteFileHookHandler {
return DeleteFileHookHandler{
pathsBuilder: pathsBuilder,
}
}

type DeleteFileHookHandler struct {
pathsBuilder func(config game_launcher.Config) ([]string, error)
}

func (h DeleteFileHookHandler) Run(fr game_launcher.FileRepository, u *url.URL, config game_launcher.Config, launchType game_launcher.LaunchType, args map[string]string) error {
paths, err := h.pathsBuilder(config)
if err != nil {
return err
}
for _, path := range paths {
if err := DeleteFileIfExists(fr, path); err != nil {
return err
}
}
return nil
}

func (h DeleteFileHookHandler) String() string {
return HookDeleteFile
}

func CoDRunningFilePathsBuilder(config game_launcher.Config) ([]string, error) {
// Running file name will be the executable name minus the .exe prefixed by __, e.g. "__CoDMP"
name := fmt.Sprintf("__%s", strings.TrimSuffix(config.ExecutableName, filepath.Ext(config.ExecutableName)))

// Primary place to look is in the install path, basically right "next to" the executable
primary := filepath.Join(config.InstallPath, name)

// At least when installed in the default location, CoD2 may store the file in the VirtualStore
virtualStore, err := buildVirtualStorePath()
if err != nil {
return nil, err
}
// With the game installed in C:\Program Files\Call of Duty, the alternate would be ...\AppData\Local\VirtualStore\Program Files\Call of Duty
alternate := filepath.Join(
virtualStore,
strings.TrimPrefix(config.InstallPath, filepath.VolumeName(config.InstallPath)),
name,
)

return []string{primary, alternate}, nil
}
108 changes: 108 additions & 0 deletions internal/titles/internal/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
package internal

import (
"fmt"
"net"
"net/url"
"path/filepath"
"strconv"
"testing"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/cetteup/joinme.click-launcher/internal/testhelpers"
"github.com/cetteup/joinme.click-launcher/pkg/game_launcher"
)

Expand Down Expand Up @@ -293,3 +296,108 @@ func TestRefractorV1CmdBuilder(t *testing.T) {
})
}
}

func TestDeleteFileHookHandler(t *testing.T) {
type test struct {
name string
givenConfig game_launcher.Config
givenPathsBuilder func(config game_launcher.Config) ([]string, error)
expect func(fr *MockFileRepository)
wantErrContains string
}

tests := []test{
{
name: "deletes CoD running file if present in install path",
givenConfig: game_launcher.Config{
InstallPath: "C:\\Program Files\\Call of Duty",
ExecutableName: "CoDMP.exe",
},
givenPathsBuilder: CoDRunningFilePathsBuilder,
expect: func(fr *MockFileRepository) {
path := "C:\\Program Files\\Call of Duty\\__CoDMP"
alternate := "AppData\\Local\\VirtualStore\\Program Files\\Call of Duty\\__CoDMP"
fr.EXPECT().FileExists(gomock.Eq(path)).Return(true, nil)
fr.EXPECT().RemoveAll(gomock.Eq(path)).Return(nil)
fr.EXPECT().FileExists(testhelpers.StringContainsMatcher(alternate)).Return(false, nil)
},
},
{
name: "deletes CoD running file if present in virtual store",
givenConfig: game_launcher.Config{
InstallPath: "C:\\Program Files (x86)\\Call of Duty 2",
ExecutableName: "CoD2MP_s.exe",
},
givenPathsBuilder: CoDRunningFilePathsBuilder,
expect: func(fr *MockFileRepository) {
primary := "C:\\Program Files (x86)\\Call of Duty 2\\__CoD2MP_s"
alternate := "AppData\\Local\\VirtualStore\\Program Files (x86)\\Call of Duty 2\\__CoD2MP_s"
fr.EXPECT().FileExists(gomock.Eq(primary)).Return(false, nil)
fr.EXPECT().FileExists(testhelpers.StringContainsMatcher(alternate)).Return(true, nil)
fr.EXPECT().RemoveAll(testhelpers.StringContainsMatcher(alternate)).Return(nil)
},
},
{
name: "does nothing if running file does not exist",
givenConfig: game_launcher.Config{
InstallPath: "C:\\Program Files\\Publisher\\Game",
ExecutableName: "Game.exe",
},
givenPathsBuilder: func(config game_launcher.Config) ([]string, error) {
return []string{filepath.Join(config.InstallPath, "Game.running")}, nil
},
expect: func(fr *MockFileRepository) {
fr.EXPECT().FileExists(gomock.Eq("C:\\Program Files\\Publisher\\Game\\Game.running")).Return(false, nil)
},
},
{
name: "errors if paths builder fails",
givenConfig: game_launcher.Config{
InstallPath: "C:\\Program Files\\Publisher\\Game",
ExecutableName: "Game.exe",
},
givenPathsBuilder: func(config game_launcher.Config) ([]string, error) {
return nil, fmt.Errorf("some-paths-builder-error")
},
expect: func(fr *MockFileRepository) {},
wantErrContains: "some-paths-builder-error",
},
{
name: "errors if file exists check fails",
givenConfig: game_launcher.Config{
InstallPath: "C:\\Program Files\\Publisher\\Game",
ExecutableName: "Game.exe",
},
givenPathsBuilder: func(config game_launcher.Config) ([]string, error) {
return []string{filepath.Join(config.InstallPath, "Game.running")}, nil
},
expect: func(fr *MockFileRepository) {
fr.EXPECT().FileExists(gomock.Eq("C:\\Program Files\\Publisher\\Game\\Game.running")).Return(false, fmt.Errorf("some-io-error"))
},
wantErrContains: "some-io-error",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// GIVEN
ctrl := gomock.NewController(t)
mockRepository := NewMockFileRepository(ctrl)
u := &url.URL{Host: net.JoinHostPort("1.1.1.1", "28960")}
handler := MakeDeleteFileHookHandler(tt.givenPathsBuilder)
args := map[string]string{}

// EXPECT
tt.expect(mockRepository)

// WHEN
err := handler.Run(mockRepository, u, tt.givenConfig, game_launcher.LaunchTypeLaunchAndJoin, args)

if tt.wantErrContains != "" {
assert.ErrorContains(t, err, tt.wantErrContains)
} else {
require.NoError(t, err)
}
})
}
}
32 changes: 32 additions & 0 deletions internal/titles/internal/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/mitchellh/go-ps"
"github.com/rs/zerolog/log"
"golang.org/x/sys/windows"

"github.com/cetteup/joinme.click-launcher/pkg/game_launcher"
)

const (
virtualStoreDirName = "VirtualStore"
)

func buildOriginURL(offerIDs []string, args []string) string {
Expand Down Expand Up @@ -80,3 +88,27 @@ func waitForProcessesToExit(processes map[int]string) error {

return nil
}

func DeleteFileIfExists(fr game_launcher.FileRepository, path string) error {
// Make sure it's a file, so we don't accidentally delete something else
exists, err := fr.FileExists(path)
if err != nil {
return err
}
if exists {
return fr.RemoveAll(path)
}
return nil
}

func GetLocalAppDataPath() (string, error) {
return windows.KnownFolderPath(windows.FOLDERID_LocalAppData, windows.KF_FLAG_DEFAULT)
}

func buildVirtualStorePath() (string, error) {
appData, err := GetLocalAppDataPath()
if err != nil {
return "", err
}
return filepath.Join(appData, virtualStoreDirName), nil
}

0 comments on commit fa05320

Please sign in to comment.